Циклический импорт сервисов: выносим сборку зависимостей в точку входа

Модуль задач импортирует файловое хранилище, чтобы создать его по умолчанию. Хранилище импортирует модель задачи из того же модуля. После очередной перестановки определений приложение падает с сообщением о 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 к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.