Структура проекта и модули
Структура проекта и модули
В предыдущем уроке мы спроектировали телефонную книгу и решили разбить её на несколько файлов. Теперь разберём, как Python работает с несколькими файлами в одном проекте, и создадим скелет нашего приложения.
Несколько файлов — несколько модулей
Каждый .py-файл в Python — это модуль. Когда проект состоит из нескольких файлов, они импортируют друг друга точно так же, как импортируют стандартную библиотеку:
# contact.py — определяет классы Contact и ContactNotFoundError
# address_book.py — импортирует из contact.py:
from contact import Contact, ContactNotFoundError
# cli.py — импортирует из обоих:
from address_book import AddressBook
from contact import Contact
Python ищет модули в:
- Текущей директории (откуда запущен скрипт)
- Директориях из
sys.path - Стандартной библиотеке
Поэтому все файлы проекта должны лежать в одной директории — тогда они находят друг друга.
Точка входа: if name == "main"
В каждом файле Python устанавливает переменную __name__. Когда файл запускают напрямую (python main.py), __name__ равен "__main__". Когда импортируют из другого файла — __name__ равен имени файла (без .py).
Конструкция if __name__ == "__main__": позволяет разделить код, который должен выполниться только при прямом запуске, от кода, который может быть импортирован:
# main.py
from cli import run_cli
def main():
run_cli()
if __name__ == "__main__":
main()
Без этой защиты main() вызывался бы при любом импорте main.py — а мы хотим, чтобы он запускался только когда пользователь явно запустит python main.py.
Скелет файлов проекта
Создадим заготовки всех файлов, чтобы понять структуру:
# contact.py
class ContactNotFoundError(Exception):
"""Контакт не найден в книге."""
pass
class Contact:
def __init__(self, name, phone, email=None):
self.name = name
self.phone = phone
self.email = email
def __str__(self):
parts = [f"{self.name}: {self.phone}"]
if self.email:
parts.append(self.email)
return ", ".join(parts)
def __repr__(self):
return f"Contact({self.name!r}, {self.phone!r}, email={self.email!r})"
# address_book.py
from contact import Contact, ContactNotFoundError
class AddressBook:
def __init__(self, filepath):
self.filepath = filepath
self._contacts = []
self._load()
def add(self, contact):
self._contacts.append(contact)
def find(self, name):
# реализация в следующем уроке
pass
def delete(self, name):
# реализация в следующем уроке
pass
def all(self):
return list(self._contacts)
def _load(self):
pass # загрузка из JSON — следующий урок
def save(self):
pass # сохранение в JSON — следующий урок
# main.py
from address_book import AddressBook
CONTACTS_FILE = "contacts.json"
def main():
book = AddressBook(CONTACTS_FILE)
print(f"Загружено контактов: {len(book.all())}")
if __name__ == "__main__":
main()
Зачем разделять
Разделение по файлам — это разделение ответственности:
contact.py— только описание данных контактаaddress_book.py— только логика работы с коллекциейcli.py— только взаимодействие с пользователемmain.py— только запуск
Если нужно изменить формат хранения (заменить JSON на SQLite), меняем только address_book.py. Если нужен другой интерфейс (веб вместо консоли), меняем только cli.py. Остальной код не трогаем.
Проверь себя
Когда выполнится main() в следующем файле?
# app.py
def main():
print("запуск")
if __name__ == "__main__":
main()
Только при прямом запуске python app.py. При импорте (from app import main) — не выполнится.
Итог
- Каждый
.py-файл — отдельный модуль; импортировать из него можно черезfrom имя import что_нужно. if __name__ == "__main__":— защита кода запуска от случайного выполнения при импорте.- Разделение по файлам = разделение ответственности; каждый файл знает о своей задаче.
- Все файлы проекта должны быть в одной директории (или корректно настроен
sys.path).
В следующем уроке реализуем классы Contact и AddressBook полностью.