# Что происходит при импорте в Python?

Аноним Автор

Давайте рассмотрим что реально происходит при импорте модуля под капотом. В основном такие детали не имеют значения, но при случае (особенно когда по ошибке вы загрузили не тот модуль), технические детали выходят на поверхность. И не мешало бы знать что происходит.


Выражение import вызывает встроенную функцию __import__() built-in.


Для импорта модуля Python использует 2 специальных объекта: finder и loader. В некоторых случаях используется объект importer, который служит как finder и loader.


Finder ответственен за локализацию модулей для импорта. Есть множество мест для поиска модулей - они не обязательно файлы - и разные ситуации, которые надо уметь обработать. Python имеет несколько типов finders для обработки разных ситуаций и это дает возможность для локализации модуля с выбранным именем.


Первое, Python использует meta path finders, который хранится в списке sys.meta_path. По умолчанию, есть три meta path finders:


• встроенный importer находит и загружает встроенные модули built-in


• frozen importer находит и загружает frozen модули, что означает модули, которые были конвертированы в компилированный байт-код


• path-based finder ищет модули по файловой системе


Содержимое sys.meta_path


[<_distutils_hack.DistutilsMetaFinder object at 0x7fb9db13a1f0>, <_virtualenv._Finder object at 0x7fb9d74a33d0>, <class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib_external.PathFinder'>]


Данный порядок поиска причина по которой вы не можете глобально переопределить встроенный модуль; встроенный importer запускается до path-based finder. Если вы хотите дополнительные meta path finder, для загрузки модулей с новой локации, вы можете добавить его как meta hook путем добавления в список sys.meta_path


Есть некоторая сложность в path-based finder, которую необходимо разобрать. Path-based finder пробует каждый из путей по очереди посредством набора хуков. Данные хуки также известные как path entry hooks хранятся в sys.path_hooks. Данный набор хуков позволяет пройти все пути согласно своей логике поиска, которые хранятся в sys.path или атрибуте __path__ текущего пакета.


Содержимое sys.path_hooks


[<class 'zipimport.zipimporter'>, <function FileFinder.path_hook.<locals>.path_hook_for_FileFinder at 0x7f7e1f08cca0>]


Модуль zipimport позволяет импортировать модули из zip архивов, что подробнее описаное вот тут https://docs.python.org/3/library/zipimport.html


Вторая же хранимая тут функция позволяет кэшировать обнаруженные при поиске пакеты https://docs.python.org/3/library/importlib.html#importlib.machinery.FileFinder


Если любой из finders локализует модуль, он возвращает объект module spec со всей информацией как загрузить модуль. Напротив, если все meta path finders возвращают None, вы получаете ошибку ModuleNotFoundError.


Как только модуль найден, module spec передается в loader, который ответственен за загрузку модуля.


Множество деталей по загрузке модулей вынесем за скобки данной статьи, но остановимся лишь на теме каким образом loader работает с кэшированным байт-кодом. Как только Python модуль был запущен, генерируется .pyc файл. Данный файл содержит байт-код, который с этого момента кэшируется. Вы часто видите эти .pyc файлы, раскиданные по директориям проекта. Loader всегда должен убедиться, что данные кэшируемые файлы не протухи на момент загрузки, для чего использует одну из двух стратегий. Первая стратегия сравнение метки времени кэша и последнего изменения исходного файла. Если они не совпадают, байт-код устарел, то исходный файл будет перекомпилирован. Вторая стратегия представлена в Python 3.7 вместо этого позволяет хранить хэш, который позволяет в коротком виде сохранять состояние исходного файла, причем в коротком и уникальном виде. Если исходный файл поменялся, то его хэш разойдется с байт-кодом. Файлы байт-кода Python хранят данный хэш в файлах hash-based.pyc.


Независимо от способа загрузки модуля он будет добавлен в sys.modules - добавлен он будет перед фактической загрузкой, чтобы предотвратить цикл импорта, если загружаемый модуль импортирует сам себя. Наконец, loader свяжет импортированный объект модуля с именем в модуле, импортирующем его, чтобы можно было сослаться на импортированный модуль.


Как только модуль был импортирован, он кэшируется в sys.path_importer_cache, среди других объектов. На самом деле это первое место, где система импорта проверит наличие импортированного модуля, даже перед запуском через средства finders, поэтому многократный импорт модуля в проекте по-прежнему будет проходить процесс поиска и загрузки только один раз.


Это слишком общее описание системы импорта, но в большинстве случаев его хватит. Для изучения более глубоких деталей, вы можете прочитать официальную документацию https://docs.python.org/3/reference/import.html.