Как работает import изнутри? importlib, sys.modules, circular imports?

import в CPython проходит через импортную машину: поиск модуля по sys.meta_path/sys.path, загрузку и исполнение кода, сохранение результата в sys.modules; повторные импорты берут модуль из кеша; importlib даёт программный доступ к этим шагам, а циклические импорты возникают из-за частично инициализированных модулей в sys.modules во время выполнения кода.
Подробный ответ

Импортная машина и 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‑импортами и выносом общих частей в отдельные модули. На практике я стараюсь проектировать модули и слои так, чтобы направленность зависимостей была односторонней и циклов не возникало.

Оцени свой прогресс

Честно оцени своё понимание этого вопроса, чтобы мы могли построить твой учебный трек максимально эффективно.
Читать в блоге