Привет, Хабр!
Сегодня говорим о том, что в какой‑то момент словит почти каждый разработчик, особенно если вы не просто пишете скрипты, а строите проекты — будь то Django, Flask или кастомная архитектура с бизнес‑логикой в отдельных слоях. Речь про круговые импорты: они не объявляют о себе заранее, не фейлят весь проект громко и сразу, но подкрадываются исподтишка. И вот вы уже сидите с ошибкой ImportError: cannot import name ...
или AttributeError
, гуглите часами, тасуете импорты туда‑сюда и ловите дежавю — кажется, это уже было, но где?
Как понять, что вы попали в петлю
Обычно все начинается невинно:
# users.py
from notifications import send_email
def register(...):
send_email(...)
# notifications.py
from users import get_user_email # ← бум!
Интерпретатор загружает users
, тот требует notifications
, а notifications
снова требует ещё не инициализированный users
. Итог — AttributeError
или ImportError
, в зависимости от того, в какой фазе успело упасть.
Классические симптомы:
В стеке вызовов видите одну и ту же пару файлов, крутящихся как белка в колесе.
Переменные‑зомби: объект уже есть в
sys.modules
, но методы ещё не определены.Flask‑кейс:
from app import app
внутри файла, который импортируется ещё до создания самого приложения.
Откуда ноги — кейсы
Flask: «app factory» и Blueprint-ы
В книгах по Flask советуют паттерн Application Factory: функцию create_app()
плюс Blueprint‑ы. Если же вы решите импортировать приложение на уровне модуля, получите «cannot import name „app“» — классический круговой бум. Лечение: создаем объекты (DB, миграции) без жесткого импорта приложения, а инициализируем их в create_app()
:
# extensions.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
# __init__.py
def create_app():
from .extensions import db
app = Flask(__name__)
db.init_app(app)
...
return app
Так мы разрываем цепь: extensions
знает о db
, но не о app
; app
инициализирует db
поздно, когда всё готово.
Django: модели, сигналы, селф-референсы
В Django импортировать модель из другой модели — грешок, особенно в signals.py
. Официальный прием:
from django.apps import apps
User = apps.get_model('auth', 'User')
или просто ссылаться строкой в ForeignKey('app.Model')
. Так объект будет резолвиться после загрузки моделей, а не во время.
Бизнес-логика: «service layer» спасет
Когда «domain» импортирует «repository», а тот обратно «domain», пора завести service layer и вынести общие типы в отдельный модуль common
, который не зависит ни от одного конкретного слоя. Так поток импортов идет строго сверху вниз.
Три кита анти-loop архитектуры
Кит № 1. Локальные импорты — быстро и грязно
Работает, но странно смотрится в код‑ревью. Пример:
def send_email(...):
from users import get_user_email
..
Убираем цикл мгновенно и экономим время старта.
Но при этом легко забыть о них, сложно мокать в тестах. Если функция вызывается часто, импорт будет валить кеш ‑ лечится контролем: if 'users' not in sys.modules: ...
.
Кит № 2. Lazy imports по PEP 690
С выходом 3.13 включаем глобальный флаг (или from future import lazy_imports
) — модули становятся «зомби‑проксами» и инициализируются при первом обращении. Цикл разрывается сам, потому что код, требующий зависимость, будет вызван после полной загрузки.
Ленивые импорты + Cinder дают −40% к времени до первого батча в ML‑воркфлоу — так что профит ощутим.
Кит № 3. Строгое разбиение слоёв
api
→service
→repository
→drivers
«низ» никогда не импортирует «верх».
Общие enum и датаклассы — в
domain
илиcommon
.
Схема банальна, но затыкает 90% круговых огрехов.
importlib: когда очень надо
import importlib
def load_plugin(name: str):
return importlib.import_module(f'app.plugins.{name}')
Подгружаем модули по строке, избегая статического цикла. Отличная точка расширения для систем плагинов.
Иногда модуль уже есть в sys.modules
, но экспорт недоступен из‑за раннего падения. Тогда:
module = importlib.reload(sys.modules['users'])
Не самый чистый паттерн, но спасает.
Если вы ещё сидите на 3.10–3.12, пишем свой «ленивый» загрузчик (документация Python привела шикарный пример):
import importlib.util as iu
import sys, types
def lazy_import(name):
spec = iu.find_spec(name)
spec.loader = iu.LazyLoader(spec.loader)
module = iu.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
Теперь lazy_import("numpy")
не прогрузит 200 МБ C‑расширений, пока вы не дёрнете numpy.array
.
Код, который не ломается
Enum-ы и DTO — в одном месте
# common/types.py
from enum import Enum
class Role(str, Enum):
ADMIN = 'admin'
USER = 'user'
users
, auth
, payments
импортируют только Role
, но не друг друга.
Фабрики вместо прямых ссылок
# services/email_service.py
def get_email_sender():
from adapters.smtp import SmtpSender
return SmtpSender(...)
Тест легко подменить через monkey‑patch:
monkeypatch.setattr(email_service, 'get_email_sender', FakeSender)
Паттерн «верхний уровень — только интерфейсы»
# repos/__init__.py
from .base import AbstractRepo
service.py
импортирует AbstractRepo
, а конкретная реализация PgRepo
регистрируется через DI‑контейнер. Таким образом, repo «знает» о service? Нет, только наоборот.
Острые уголки
«Локальные импорты — это некрасиво». Согласен, но когда горит, стоит выбрать рабочее и уродливое» вместо «красивое, но лежит».
Lazy imports скрывают ошибки: исключения всплывают не на старте, а в произвольный момент. Логируйте
importlib.invalidate_caches()
на CI и гоняйтеpytest -Werror
.Статический анализ (mypy, pylint) может не видеть динамику. Решение —
typing.TYPE_CHECKING
и псевдо‑импорт:from typing import TYPE_CHECKING if TYPE_CHECKING: from service import ImportantClass
Рефакторинг сто строк vs. ручка
reload
. Я за рефакторинг: долгосрочно дешевле.
Если вы дочитали до этого места, скорее всего, уже сталкивались с круговыми импортами или как минимум с болью архитектурных костылей. А теперь представьте: ваш код не просто не ломается — он устойчив, масштабируем и готов к росту команды.
Чтобы к этому прийти — стоит не просто гуглить ошибки, а копать глубже. Мы в OTUS как раз для этого сделали два открытых урока. Без «воды», с практикой и пользой:
Основы аннотаций типов в Python — 26 июня в 20:00
Шик, блеск, чистота: clean architecture в Python — 1 июля в 20:00
А если после уроков вы захотите двигаться дальше — загляните на страницу курса «Специализация Python Developer». Мы постарались сделать программу максимально прикладной и ориентированной на реальную разработку.