Импортная машина и sys.modules
Когда интерпретатор видит import package.module as m, он делает примерно следующее:
проверяет, есть ли модуль в sys.modules; если да, просто возвращает уже загруженный объект;
если нет, запускает импортную машину: проходит по sys.meta_path, вызывая find_spec у finder’ов, чтобы найти спецификацию модуля (откуда и как его грузить);
использует loader из spec, чтобы загрузить модуль (прочитать файл, скомпилировать, выполнить байткод);
создаёт объект модуля, регистрирует его в sys.modules ещё до выполнения кода, затем исполняет его тело в пространстве имён модуля.
Ключевой момент: модуль попадает в sys.modules до того, как его код полностью выполнен. Это важно для понимания циклических импортов.
importlib
Пакет importlib даёт программный доступ к импортной машине:
importlib.import_module(name) — эквивалент __import__, но с более приятным API;
importlib.util.find_spec, importlib.machinery — работа со spec, кастомные loader’ы и finder’ы;
reload модулей, динамический импорт по строке и т.п.
На уровне senior полезно понимать importlib хотя бы настолько, чтобы:
делать ленивый/отложенный импорт (для снижения времени старта или избегания тяжёлых зависимостей);
писать плагинообразные системы, подгружающие модули по имени/пути.
Циклические импорты (circular imports)
Циклический импорт возникает, когда:
модуль A при инициализации импортирует B;
модуль B при инициализации импортирует A;
один из модулей в момент обращения к атрибутам другого видит его в частично инициализированном состоянии (атрибутов ещё нет).
Поскольку модуль регистрируется в sys.modules до полного исполнения кода, второй участник цикла может получить объект модуля, в котором ещё не выполнены нижние строки, и попытка взять из него что‑то (класс, функцию) приведёт к AttributeError или неожиданному поведению.
Как обходить циклические импорты
переносить импорт внутрь функции/метода (lazy import) там, где это допустимо по производительности;
выделять общий код в третий модуль и импортировать его из обоих;
реорганизовывать архитектуру: отделять слои (API, сервисы, репозитории, модели), избегая двунаправленных зависимостей между слоями;
для аннотаций типов использовать строковые типы или from __future__ import annotations, чтобы не создавать реальные зависимости на момент импорта.
Формулировка уровня senior
В Python импорт реализован через импортную машину: модуль ищется через sys.meta_path/sys.path, загружается loader’ом, регистрируется в sys.modules и после этого исполняется. sys.modules — это кеш уже загруженных модулей, из которого берутся все последующие импорты. Пакет importlib даёт программный доступ к этим механизмам — динамический импорт, поиск spec, кастомные загрузчики. Циклические импорты возникают из‑за того, что модуль может быть виден в sys.modules ещё до завершения инициализации; решаются они реорганизацией зависимостей, lazy‑импортами и выносом общих частей в отдельные модули. На практике я стараюсь проектировать модули и слои так, чтобы направленность зависимостей была односторонней и циклов не возникало.