Циклический импорт сервисов: выносим сборку зависимостей в точку входа
Модуль задач импортирует файловое хранилище, чтобы создать его по умолчанию. Хранилище импортирует модель задачи из того же модуля. После очередной перестановки определений приложение падает с сообщением о partially initialized module. Порядок импортов стал скрытым условием корректности программы.
Перенос import внутрь метода иногда убирает конкретное падение, но сохраняет зависимость между частями приложения. Более устойчивое исправление — отделить модель и контракт от сборки. Сервис получает готовое хранилище в конструкторе. Точка входа знает обе реализации и соединяет их, а предметная операция не создаёт файловый адаптер самостоятельно.
Зависимости собираются снаружи
Для небольшого примера достаточно одного файла, но границы видны по определениям. Task и Store могут лежать в domain.py, MemoryStore — в adapters.py, TaskService — в service.py. Только main.py импортирует сервис и адаптер одновременно. Ни domain.py, ни service.py не должны импортировать main.py, иначе цикл вернётся через сборку.
from dataclasses import dataclass
from typing import Protocol
@dataclass
class Task:
title: str
class Store(Protocol):
def add(self, task: Task) -> None: ...
class MemoryStore:
def __init__(self) -> None:
self.tasks: list[Task] = []
def add(self, task: Task) -> None:
self.tasks.append(task)
class TaskService:
def __init__(self, store: Store) -> None:
self.store = store
def create(self, title: str) -> None:
self.store.add(Task(title))
store = MemoryStore()
TaskService(store).create("report")
assert [task.title for task in store.tasks] == ["report"]
print([task.title for task in store.tasks])
Проверяем направление импортов
Запустите файл: он выведет ['report']. Здесь проверяется наблюдаемый эффект — сервис передал задачу в выбранное хранилище. Следующим тестом подставьте адаптер, который выбрасывает OSError, и определите, должна ли ошибка выйти наружу или превратиться в предметное исключение. Привязка к интерфейсу не освобождает от решения, где проходит граница обработки отказов.
Когда разносите определения по файлам, сначала проверьте импорты из нового процесса. Старый интерактивный интерпретатор мог сохранить уже загруженные модули в sys.modules и скрыть проблему. Импорт каждого публичного модуля отдельно позволяет заметить, что один из них всё ещё требует случайного предшественника. Запуск main проверяет сборку целиком.
Не нужно создавать фабрику на каждую строку и контейнер зависимостей ради трёх объектов. Главная перемена состоит в направлении зависимостей: бизнес-операция знает, что ей нужно, адаптер предоставляет реализацию, точка входа выбирает конкретные объекты. Явный конструктор хорошо показывает эту связь без дополнительных библиотек.
После исправления сервис можно проверить с памятью вместо файла, а приложение по-прежнему запускать с файловым хранилищем. Разделение важно и для времени жизни ресурсов: точка входа открывает хранилище, выполняет работу и закрывает его. Циклический импорт исчезает потому, что модули перестают одновременно определять правила и выбирать собственные внешние зависимости.
Следующий шаг — перенести этот приём из отдельного файла в поддерживаемое приложение. В курсе «Python для продвинутых: ООП, типизация и тестирование» структура пакетов, объектная модель, mypy и pytest соединяются в типизированном трекере задач с хранением данных и логированием. Можно отработать связь между интерфейсом, реализацией и регрессионным тестом, а затем применить её к своему проекту.
Посмотреть программу и практику курса.
Документация: официальный источник по теме.
Другие задачи по теме: Типы, тесты и поведение программы.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
