Python import, как и для чего?
В языке программирования Python подключение пакетов и модулей осуществляется с помощью import. Это позволяет распределять код по логическим «узлам» приложения(модели данных, обработчики, и тп.), что позволяет получить менее нагруженные кодом файлы.
Повышается читаемость кода.
Код логически разбит по «узлам», его поиск и дальнейший отлов ошибок становится понятнее и проще.
Для разработки в команде это дает более четкое понимание, что и где делает каждый при выполнении «задания».
Нужно понимать, что делается и для чего.
Как использовать import?
Синтаксис import в Python достаточно прост и интуитивно понятен:
Что можно импортировать?
Для более глубокого понимания import стоит рассмотреть пример, представленный ниже.
Красиво, читаемо и понятно.
В чем же подвох?
Но даже в таком простом примере есть подвох, о котором многие не догадываются(если вы начинающий программист, то лучше перейдите к следующему оглавлению).
Идеология Python достаточно интересна, что позволяет ему иметь низкий порог вхождения, низкое время написания кода, высокую читаемость, но именно в ней и кроется подвох.
По своему опыту использования данного языка, сложилось отчетливое ощущение главной идеи ООП(все есть объект). Что же в этом плохого?
Все файлы, функции и тд. это объект. Но что это за объект и класс стоят за файлами(модулями)?
Все просто, это любимый всеми программистами класс, использующий паттерн проектирования Singleton.
Поэтому при достаточно ветвистой структуре, импорт переменной и дальнейшая ее модификация может порождать достаточно не простые в понимании баги(переменная в своем цикле жизни может иметь любое значение и никаких гарантий нет).
Ветвистая структура приложения и существующие подходы импортирования
Часто в разработке приложений программисты пытаются разбить программу по логическим «узлам». Данный подход повышает читаемость и позволяет вести разработку в команде(один человек занимается реализацией одного «узла», второй другого). Так порождается структура приложения, которая зачастую виду сложности функционала является достаточно обширной(ветвистой, потому что имея одну точку входа откуда уже обрастая функционалом это становится похожим на дерево).
Подключение модулей в Python
Язык программирования Python имеет много встроенных функций. Однако их не хватает для решения всех видов задач, поэтому программисты добавляют инструменты, подключая модули.
Что такое модуль
Это отдельный файл, содержащий какой-то код. Любой скрипт, написанный программистом на Python 3, можно назвать модулем. Он может быть как исполняемым, так и подключаемым. Исполняемый модуль содержит код, который самостоятельно выполняет какие-то действия, а подключаемый представляет из себя набор функций, классов и объектов, которые можно использовать для решения задач в другой программе.
Разделение программ на модули даёт ряд преимуществ:
- Программа имеет понятную структуру, что позволяет программисту легко ориентироваться в тысячах строк кода.
- Позволяют использовать одни и те же имена для функций, то есть не допускают пересечений имён.
- Программисту не нужно загружать скрипт большим количеством функций, большая часть которых не используется, достаточно подключить только нужные модули или их части.
Исполняемый и подключаемый модуль
Можно написать такой скрипт, который будет и выполнять какие-то действия (программа), и импортироваться в другие модули (библиотека).
Его важно правильно оформить:
- Весь исполняемый код помещается в функцию main() .
- Функция main() вызывается после условия:
Подключение модуля
Это можно сделать разными способами, выбор зависит только от нужд и желаний программиста.
Модули в Python — это файлы с расширением «.py». При импорте расширение опускается, интерпретатор и так знает, что после команды import следует имя модуля.
Программист может без проблем подключить любой модуль, который есть в стандартной библиотеке Python 3. Для подключения специфичных пользовательских инструментов сначала нужно их скачать. Обычно для этого используется пакетный менеджер pip.
Обычное подключение — import
Подключать модули желательно в самом вверху скрипта, используя ключевое слово «import», например import random .
После подключения программа получает доступ ко всем функциям, методам и классам, содержащимся в нём.
Программист может вызвать любую функцию из подключенной библиотеки используя префикс « имя_модуля. «. Пример: random.randint(1,15) где random — это библиотека, которую мы подключили, а randint — имя функции, которая в ней описана.
Этот способ не допускает пересечения имён, то есть программист может использовать одно и то же имя функции в скрипте, точно такое же, как и в подключаемой библиотеке и не бояться, что после её подключения, функция будет переопределена.
Вот полный пример использования инструкции import в Python 3:
Использование псевдонимов — as
Некоторые модули имеют длинное и неудобное название. Для удобства и сокращения количества кода программист может заменить его на своё.
Например, если написать « import random as rand «, то вместо длинного random для обращения к функциям библиотеки можно будет использовать короткое rand .
Импорт компонентов — from
Чтобы не захламлять программу большим количеством неиспользуемых инструментов, можно подключать не весь модуль, а какую-то его часть.
Например, программист хочет использовать только одну функцию из математической библиотеки math . Если он подключит всю библиотеку, то в скрипт добавится более 40 функций, которые будут занимать место. Чтобы добавить в проект какую-то часть, используют ключевое слово from :
Таким образом, основной скрипт получает доступ только к определённой функции. Кроме того, при таком подключении при вызове функций из подключённого модуля не используется префикс. Важно не забывать об этом, чтобы не допустить конфликта имён.
Можно подключить несколько функций сразу в одной строке. Для этого их надо перечислить через запятую.
Если после import написать символ звёздочки «*», подключится все содержимое модуля. Это считается плохим тоном, потому что может привести к совпадению имён из основного скрипта с именами из подключаемого. Но если программист уверен, что использовал уникальные названия для функций и переменных, теоретически он может использовать этот способ.
Перезагрузка библиотеки
За один сеанс модуль можно импортировать только один раз. Если программист после импорта, изменит в файле, который импортировал что-либо, а потом снова его импортирует, основная программа не будет видеть этих изменений.
Всё потому, что при импорте библиотека кешируется, когда её пытаются импортировать снова, интерпретатор Python просто использует сохранённую в кэше копию.
Если всё же необходимо перезагрузить модуль, на помощью приходит функция reload() из стандартной библиотеки importlib. Перезагрузка не влияет на объекты, ссылающиеся на импортированный модуль, и позволяет реализовать динамическую перезагрузку компонентов программы.
Подключение из другой папки
Библиотеки подключаются очень просто, когда интерпретатор Python знает, где их искать. Python ищет модули:
- В папке исполняемого скрипта.
- Во встроенных модулях (built-in).
- В директориях, определённых в sys.path (также содержит переменную PYTHONPATH).
Чтобы импортировать модуль из другой папки в Python 3, можно сделать следующее:
- Добавить путь к папке с модулем с помощью команды библиотеки sys —
- Или переместить модуль в папку, определённую в путях поиска интерпретатора Python.
Для того чтобы директория, содержащая файлы, определялась как пакет, в неё необходимо добавить файл __init__.py. Он показывает интерпретатору Python, что папка — это пакет с модулями.
Начиная с версии Python 3.3, добавлять файл __init__.py в директорию больше не нужно, интерпретатор Python считает все папки пакетами.
Основы
В Python ключевое слово import применяется для того, чтобы сделать код в одном модуле доступным для работы в другом. Импорт в Python важен для эффективного структурирования кода. Правильное применение импорта повысит вашу продуктивность: вы сможете повторно использовать код и при этом продолжать осуществлять поддержку своих проектов.
В статье представлен подробный обзор инструкции import в Python и того, как она работает. Здесь мощная система импорта. Вам предстоит узнать, как эту мощь задействовать, а также изучить ряд понятий, лежащих в основе системы импорта в Python. Их изложение в статье построено главным образом на примерах (в помощь вам будут несколько примеров кода).
В этой статье вы узнаете, как:
- Работать с модулями, пакетами и пакетами пространств имён.
- Импортировать ресурсы и файлы данных внутри ваших пакетов.
- Динамически импортировать модули во время выполнения.
- Настраивать систему импорта в Python.
На протяжении всей статьи даются примеры: вы сможете поэкспериментировать с тем, как организован импорт в Python, чтобы работать наиболее эффективно. Хотя в статье показан весь код, имеется также возможность скачать его по ссылке ниже:
Базовый импорт Python
Код в Python организован в виде модулей и пакетов. В этой части статьи мы объясним, чем они отличаются друг от друга и как с ними можно работать.
Чуть дальше вы узнаете о нескольких продвинутых и менее известных примерах применения системы импорта в Python. Но начнём с основ — импортирования модулей и пакетов.
Модули
В Python.org glossary даётся следующее определение модуля:
Объект, который служит организационной единицей кода в Python. Модули имеют пространство имён, в котором содержатся произвольные объекты Python. Модули загружаются в Python посредством импортирования. (Источник)
На практике модуль соответствует, как правило, одному файлу с расширением .py . В этом файле содержится код на Python.
Модули обладают сверхспособностью импортироваться и повторно использоваться в другом коде. Рассмотрим следующий пример:
В первой строке import math вы импортируете код в модуль math и делаете его доступным для использования. Во второй строке вы получаете доступ к переменной в модуле math . Модуль math является частью стандартной библиотеки Python, поэтому он всегда доступен для импорта, когда вы работаете с Python.
Обратите внимание, что пишется не просто pi , а math.pi .
math — это не только модуль, а ещё и пространство имён, в котором содержатся все атрибуты этого модуля. Пространства имён важны для читаемости и структурированности кода.
Содержимое пространства имён можно посмотреть с помощью dir() :
Если не указывать при этом никаких аргументов, т.е. напечатать просто dir() , то можно увидеть, что находится в глобальном пространстве имён. Посмотреть содержимое пространства имён math можно, указав его в качестве аргумента вот так: dir(math) .
Вы уже видели самый простой способ импортирования. Есть и другие, которые позволяют импортировать отдельные части модуля и переименовывать его в процессе импортирования.
Вот код, который импортирует из модуля math только переменную pi :
Обратите внимание, что pi помещается в глобальное пространство имён, а не в пространство имён math .
А вот как в процессе импортирования переименовываются модули и атрибуты:
Пакеты
Пакет представляет собой следующий после модуля уровень в организационной иерархии кода. В Python.org glossary даётся следующее определение пакета:
Это модуль Python, который может содержать подмодули или (рекурсивно) подпакеты. Строго говоря, пакет — это модуль Python с атрибутом __path__ . (Источник.)
То есть пакет — это тоже модуль. Пользователю обычно не приходится задумываться о том, что у него импортируется: модуль или пакет.
На практике пакет — это, как правило, каталог файлов, внутри которого находятся файлы Python и другие каталоги. Чтобы создать пакет Python самостоятельно, создайте каталог, а внутри него — файл с именем __init__.py . В __init__.py файле находится содержимое этого пакета-модуля. И он может быть пустым.
Обратите внимание: каталоги без файла __init__.py Python всё равно считает пакетами. Но это уже будут не обычные пакеты, а то, что можно назвать пакетами пространства имён. Подробнее о них чуть дальше в статье.
Вообще подмодули и подпакеты нельзя импортировать вместе с пакетом. Это можно сделать с помощью __init__.py , включив любой или все подмодули и подпакеты, если захотите. В качестве примера создадим пакет для Hello world на разных языках. Пакет будет состоять из следующих каталогов и файлов:
Для файла каждой страны выводится соответствующее приветствие, а файлы __init__.py выборочно импортируют некоторые подпакеты и подмодули. Вот точное содержимое этих файлов:
Обратите внимание: world/__init__.py импортирует только africa , а не europe ; world/africa/__init__.py ничего не импортирует; world/europe/__init__.py импортирует greece и norway , а не spain . Модуль каждой страны при импортировании выводит приветствие.
Разберёмся, как ведут себя подпакеты и подмодули в пакете world :
При импортировании europe модули europe.greece и europe.norway тоже импортируются. Это происходит потому, что модули этих стран выводят приветствие при импортировании:
Файл world/africa/__init__.py пуст. Это означает, что импортирование пакета world.africa создаёт пространство имён, но этим и ограничивается:
Не забывайте: при импорте модуля загружается его содержимое и одновременно создаётся пространство имён с этим содержимым. Последние несколько примеров показывают, что один и тот же модуль может быть частью разных пространств имён.
Технические нюансы: пространство имён модуля реализовано в виде словаря Python и доступно в атрибуте .__dict__ :
Но вам не придётся часто взаимодействовать с .__dict__ напрямую.
Глобальное пространство имён в Python тоже является словарём. Доступ к нему можно получить через globals() .
Импортировать подпакеты и подмодули в файле __init__.py — это обычное дело. Так они становятся более доступными для пользователей. Вот вам пример того, как это происходит в популярном пакете запросов.
Абсолютный и относительный импорт
Напомним исходный код world/__init__.py предыдущего примера:
Чуть ранее мы уже разбирали операторы типа from. import , такие как from math import pi . Что же означает точка ( . ) в from . import africa ?
Точка указывает на текущий пакет, а сам оператор — это пример относительного импорта. Можно прочитать этот
так: «из текущего пакета импортируется подпакет africa ».
Существует эквивалентный ему оператор абсолютного импорта, в котором прямо указывается название этого текущего пакета:
На самом деле, все импорты в world можно было бы сделать в виде таких вот абсолютных импортов с указанием названия текущего пакета.
Относительные импорты должны иметь такую from. import форму, причём обозначение места, откуда вы импортируете, должно начинаться с точки.
В руководстве по стилю PEP 8 рекомендуется в основном абсолютный импорт. Однако относительный импорт в качестве альтернативы абсолютному тоже имеет право на существование при организации иерархии пакетов.
Путь импорта в Python
А как Python находит модули и пакеты, которые импортирует? Более подробно о специфике системы импорта в Python расскажем чуть дальше в статье. А пока нам достаточно просто знать, что Python ищет модули и пакеты в своём пути импорта. Это такой список адресов, по которым выполняется поиск модулей для импорта.
Примечание: когда вы вводите import чего-то (что надо импортировать) , Python будет искать это что-то в нескольких разных местах, прежде чем переходить к поиску пути импорта.
Так, он заглянет в кэш модулей и проверит, не было ли это что-то уже импортировано, а также проведёт поиск среди встроенных модулей.
Подробнее о том, как организован импорт в Python, расскажем чуть дальше в статье.
Путь импорта в Python можно просмотреть, выведя на экран sys.path . В этом списке будет три различных типа адресов:
- Каталог текущего скрипта или текущий каталог, если скрипта нет (например, когда Python работает в интерактивном режиме).
- Содержимое переменной окружения PYTHONPATH .
- Другие каталоги, зависящие от конкретной системы.
Поиск Python, как правило, стартует в начале списка адресов и проходит по всем адресам до первого совпадения с искомым модулем. Каталог скрипта или текущий каталог всегда идёт первым в этом списке. Поэтому можно организовать каталоги так, чтобы скрипты находили ваши самодельные модули и пакеты. При этом надо внимательно следить за тем, из какого каталога вы запускаете Python.
Стоит следить и за тем, чтобы не создавались модули, которые затеняют или скрывают другие важные модули. В качестве примера предположим, что вы определяете следующий модуль math :
Всё пока идёт как надо:
Вот только модуль этот затеняет модуль math , который входит в состав стандартной библиотеки. Это приводит к тому, что наш предыдущий пример поиска значения π больше не работает:
Вместо того, чтобы искать модуль math в стандартной библиотеке, Python теперь ищет ваш новый модуль math для pi .
Во избежание подобных проблем надо быть осторожным с названиями модулей и пакетов. Имена модулей и пакетов верхнего уровня должны быть уникальными. Если math определяется как подмодуль внутри пакета, то он не будет затенять встроенный модуль.
Структурируем импорт
Несмотря на то, что мы можем организовать импорт, используя текущий каталог, переменную окружения PYTHONPATH и даже sys.path , этот процесс часто оказывается неконтролируемым и подверженным ошибкам. Типичный пример даёт нам следующее приложение:
Приложение воссоздаст данную файловую структуру с каталогами и пустыми файлами. Файл structure.py содержит основной скрипт, а files.py — это библиотечный модуль с функциями для работы с файлами. Вот что выводит приложение, запускаемое в данном случае в каталоге structure :
Два файла исходного кода плюс автоматически созданный файл .pyc повторно создаются внутри нового каталога с именем 001 .
Обратимся теперь к исходному коду. Основная функциональность приложения определяется в structure.py :
В строках с 12 по 16 читается корневой путь из командной строки. Точкой здесь обозначается текущий каталог. Этот путь — root файловой иерархии, которую вы воссоздадите.
Вся работа происходит в строках с 19 по 23. Сначала создаётся уникальный путь new_root , который будет корневым каталогом новой файловой иерархии. Затем в цикле проходятся все пути ниже исходного root , и они воссоздаются в виде пустых файлов внутри новой файловой иерархии.
В строке 26 вызывается main() . О проверке условия if в строке 25 подробнее узнаем дальше в статье. А пока нам достаточно знать, что специальная переменная __name__ внутри скриптов имеет значение __main__ , а внутри импортируемых модулей получает имя модуля.
Обратите внимание: в строке 8 импортируются файлы . В этом библиотечном модуле содержатся две служебные функции:
unique_path() работает со счётчиком для обнаружения пути, которого уже не существует. В приложении он нужен, чтобы найти уникальный подкаталог, который будет использоваться в качестве new_root вновь созданной файловой иерархии. add_empty_file() обеспечивает создание всех необходимых каталогов до того, как с помощью .touch() будет создан пустой файл.
Ещё раз взглянем на импорт файлов :
Выглядит он совершенно невинно. Однако по мере роста проекта эта строка станет источником некоторых проблем. Даже если импорт файлов происходит из проекта structure , этот импорт абсолютный: он не начинается с точки. А это означает, что файлы должны быть найдены в пути импорта, чтобы импорт состоялся.
К счастью, каталог с текущим скриптом всегда находится в пути импорта Python. Так что, пока проект не набрал обороты, импорт работает нормально. Но дальше возможны варианты.
Например, кто-то захочет импортировать скрипт в Jupyter Notebook и запускать его оттуда. Или иметь доступ к библиотеке файлов в другом проекте. Могут даже с помощью PyInstaller создавать исполняемые файлы, чтобы упростить их дальнейшее распространение. К сожалению, любой из этих сценариев может вызвать проблемы с импортом файлов.
Каким образом? Вот вам пример. Возьмём руководство по PyInstaller и создадим точку входа в приложение. Добавим дополнительный каталог за пределами каталога приложения:
В этом внешнем каталоге создадим скрипт точки входа cli.py :
Этот скрипт импортирует из исходного скрипта main() и запускает его. Обратите внимание: когда импортируется structure , main() не запускается из-за проверки условия if в строке 25 внутри structure.py . То есть нужно запускать main() явным образом.
По идее, это должно быть аналогично прямому запуску приложения:
Почему же запуск не удался? При импорте файлов неожиданно возникает ошибка.
Проблема в том, что при запуске приложения с cli.py поменялся адрес текущего скрипта, а это, в свою очередь, меняет путь импорта. Файлы больше не находятся в пути импорта, поэтому их абсолютный импорт невозможен.
Одно из возможных решений — поменять путь импорта Python. Вот так:
Здесь в пути импорта есть папка со structure.py и files.py . Поэтому это решение работает. Но такой подход неидеален, ведь путь импорта может стать очень неаккуратным и трудным для понимания.
Фактически происходит воссоздание функции ранних версий Python, называемой неявным относительным импортом. Она была удалена из языка в руководстве по стилю PEP 328 со следующим обоснованием:
В Python 2.4 и более ранних версиях при чтении модуля, расположенного внутри пакета, неясно: относится ли import foo к модулю верхнего уровня или к другому модулю внутри пакета. По мере расширения библиотеки Python всё больше и больше имеющихся внутренних модулей пакета вдруг случайно затеняют модули стандартной библиотеки. Внутри пакетов эта проблема усугубляется из-за невозможности указать, какой модуль имеется в виду. (Источник.)
Другое решение — использовать вместо этого относительный импорт. Меняем импорт в structure.py :
Теперь приложение можно запустить через скрипт точки входа:
Но вызвать напрямую приложение больше не получится:
Проблема в том, что относительный импорт разрешается в скриптах иначе, чем импортируемые модули. Конечно, можно вернуться и восстановить абсолютный импорт, а затем выполнить непосредственный запуск скрипта или даже попытаться провернуть акробатический трюк с try. except и реализовать абсолютный или относительный импорт файлов (в зависимости от того, что сработает).
Есть даже официально санкционированный хакерский приём, позволяющий работать с относительным импортом в скриптах. Вот только в большинстве случаев при этом придётся менять sys.path . Цитируя Реймонда Хеттинджера, можно сказать:
И действительно, лучшее (и более стабильное) решение — поэкспериментировать с системой управления пакетами и импорта Python, устанавливая проект в качестве локального пакета с помощью pip .
Создание и установка локального пакета
При установке пакета из PyPI этот пакет становится доступным для всех скриптов в вашей среде. Но пакеты можно установить и с локального компьютера, и они точно так же будут доступны.
Создание локального пакета не приводит к большому расходу вычислительных ресурсов. Сначала создаём минимальный набор файлов setup.cfg и setup.py во внешнем каталоге structure :
Теоретически name и version могут быть любыми. Надо лишь учесть, что они задействованы pip при обращении к пакету, поэтому стоит выбрать для него значения, легко узнаваемые и выделяющие его из массы других пакетов.
Рекомендуется давать всем таким локальным пакетам общий префикс, например local_ или ваше имя пользователя. В пакетах должен находиться каталог или каталоги, содержащие исходный код. Теперь можно установить пакет локально с помощью pip :
Эта команда установит пакет в вашу систему. structure после этого будет находиться в пути импорта Python. То есть можно будет выполнить её в любом месте, не беспокоясь о каталоге скрипта, относительном импорте или других сложностях. -e означает editable (редактируемый). Это важная опция, позволяющая менять исходный код пакета без его переустановки.
Примечание: такой установочный файл отлично подходит для самостоятельной работы с проектами. Если же вы планируете поделиться кодом ещё с кем-то, то стоит добавить в установочный файл кое-какую дополнительную информацию.
Теперь, когда structure в системе установлена, можно использовать следующую инструкцию импорта:
Она будет работать независимо от того, чем закончится вызов приложения.
Совет: старайтесь разделять в коде скрипты и библиотеки. Вот хорошее практическое правило:
- Скрипт предназначен для запуска.
- Библиотека предназначена для импорта.
Возможно, у вас есть код, который вы хотите запускать самостоятельно и импортировать из других скриптов. На этот случай стоит провести рефакторинг кода, чтобы разделить общую часть на библиотечный модуль.
Разделять скрипты и библиотеки — неплохая идея, тем не менее в Python все файлы можно запускать и импортировать. Ближе к завершению статьи подробнее расскажем о том, как создавать модули, которые хорошо справляются и с тем, и с другим.
Пакеты пространства имён
Модули и пакеты в Python очень тесно связаны с файлами и каталогами. Это отличает Python от многих других языков программирования, в которых пакеты — это не более чем пространства имён без обязательной привязки к тому, как организован исходный код. Для примера можете ознакомиться с обсуждением на PEP 402.
Пакеты пространства имён доступны в Python с версии 3.3. Они в меньшей степени зависят от имеющейся здесь файловой иерархии. Так, пакеты пространств имён могут быть разделены на несколько каталогов. Пакет пространства имён создаётся автоматически, если у вас есть каталог, содержащий файл .py , но нет __init__.py . Подробное объяснение смотрите в PEP 420.
Замечание: справедливости ради стоит отметить, что пакеты неявных пространств имён появились в Python 3.3. В более ранних версиях Python пакеты пространств имён можно было создавать вручную несколькими различными несовместимыми способами. Все эти ранние подходы обобщены и в упрощённом виде представлены в PEP 420.
Для лучшего понимания пакетов пространства имён попробуем реализовать один из них. В качестве поясняющего примера рассмотрим такую задачу. Дано: объект Song . Требуется преобразовать его в одно из строковых представлений. То есть нужно сериализовать объекты Song .
А конкретнее — нужно реализовать код, который работает примерно так:
Предположим, нам повезло наткнуться на стороннюю реализацию нескольких форматов, в которые нужно сериализовать объекты, и она организована как пакет пространства имён:
В файле json.py содержится код, который может сериализовать объект в формат JSON:
Этого несколько ограниченного интерфейса сериализатора будет достаточно, чтобы продемонстрировать, как работают пакеты пространства имён.
В файле xml.py содержится аналогичный XmlSerializer , который может преобразовать объект в XML:
Обратите внимание, что оба этих класса реализуют один и тот же интерфейс с помощью методов .start_object() , .add_property() и .__str__() .
Затем создаём класс Song , который может применять эти сериализаторы:
Song (песня) определяется по идентификатору, названию и исполнителю. Обратите внимание, что .serialize() не нужно знать, в какой формат происходит преобразование, потому что он использует общий интерфейс, определённый ранее.
Установив пакет сторонних serializers , можно работать с ним так:
Для разных объектов сериализатора, вызывая .serialize() получаем разные представления песни.
Примечание: при запуске кода можно получить ModuleNotFoundError или ImportError . Всё потому, что serializers нет в пути импорта Python. Но скоро мы увидим, как решить эту проблему.
Пока все идёт хорошо. Но теперь песни нужно преобразовать и в представление YAML, которое не поддерживается сторонней библиотекой. Тут-то в дело и вступают пакеты пространства имён: можем добавить в пакет serializers собственный YamlSerializer , не прибегая к сторонней библиотеке.
Сначала создаём каталог в локальной файловой системе под названием serializers . Важно, чтобы имя каталога совпадало с именем настраиваемого пакета пространства имён:
В файле yaml.py определяем собственный YamlSerializer . Делаем это с помощью пакета PyYAML , который должен быть установлен из PyPI:
Форматы YAML и JSON очень похожи, поэтому здесь можно повторно использовать большую часть реализации JsonSerializer :
Смотрите: YamlSerializer здесь основан на JsonSerializer , который импортируется из этих самых serializers . А раз json и yaml являются частью одного и того же пакета пространства имён, то мы можем даже использовать относительный импорт: from .json import JsonSerializer .
Поэтому, продолжая этот пример, мы теперь можем преобразовать песню в YAML:
Подобно обычным модулям и пакетам, пакеты пространства имён должны находиться в пути импорта Python. Если бы мы делали, как в предыдущих примерах, то могли бы столкнуться с проблемами: Python не находил бы serializers . В реальном коде мы бы использовали pip для установки сторонней библиотеки, так что они автоматически оказывались бы в нашем пути.
Примечание: в исходном примере выбор сериализатора делался более динамично. Позже мы увидим, как использовать пакеты пространств имён в соответствующем шаблоне «фабричный метод».
И нужно позаботиться о том, чтобы локальная библиотека была доступна так же, как и обычный пакет. Как мы уже убедились, это можно сделать либо запустив Python из соответствующего каталога, либо опять-таки используя pip для установки локальной библиотеки.
В этом примере мы тестируем, как можно интегрировать фейковый сторонний пакет с нашим локальным пакетом. Будь сторонний third_party реальным пакетом, то мы бы загрузили его из PyPI с помощью pip . А так мы можем сымитировать его, установив third_party локально, как уже было сделано ранее в примере со structure .
Или же можно поколдовать с путём импорта. Поместите каталоги third_party и local в одну папку, а затем настройте путь Python вот так:
Теперь можно использовать все сериализаторы, не беспокоясь о том, где они определены: в стороннем пакете или локально.
Руководство по стилю импорта
В руководстве по стилю Python PEP 8 есть ряд рекомендаций, касающихся импорта. Как всегда, в Python важное значение придаётся читаемости и лёгкости сопровождения кода. Вот несколько общих практических правил относительно того, какого стиля надо придерживаться при оформлении импорта:
- Находится в верхней части файла.
- Прописывается в отдельных строках.
- Организуется в группы: сначала идут импорты стандартной библиотеки, затем сторонние импорты, а после — импорты локальных приложений или библиотек.
- Внутри каждой группы импорты располагаются в алфавитном порядке.
- Предпочтение отдаётся абсолютному импорту над относительным.
- Импорты со спецсимволами типа звёздочки ( from module import * ) стараются не использовать.
Инструменты isort и reorder-python-imports отлично подходят для реализации этих рекомендаций в последовательном стиле импорта. Вот пример раздела импорта внутри пакета Real Python feed reader package:
Обратите внимание на чёткую организацию по группам. Сразу позволяет обозначить зависимости этого модуля, которые должны быть установлены: feedparser и html2text . Обычно подразумевается, что стандартная библиотека доступна. Разделение импортов внутри пакета даёт некоторое представление о внутренних зависимостях кода.
Бывают случаи, когда имеет смысл немного отойти от этих правил. Мы уже видели, что относительный импорт может быть альтернативой при организации иерархии пакетов. В конце статьи мы увидим, как в некоторых случаях можно переместить импорт в определение функции, чтобы прервать циклы импорта.
Импорт в Python. Ресурсы и динамический импорт
Иногда наш код зависит от файлов данных или других ресурсов. В небольших скриптах это не проблема — мы можем указать путь к файлу данных и продолжить работу!
Однако, если файл ресурсов важен для нашего пакета и хочется поделиться им с другими пользователями, возникает несколько проблем:
- У нас не будет контроля над путём к ресурсу, так как это будет зависеть от настроек пользователя, а также от того, как пакет распространяется и устанавливается. Можно попробовать узнать путь к ресурсу с помощью атрибутов пакета __file__ или __path__ , но такой способ не всегда может сработать так, как мы ожидаем.
- Пакет может находиться внутри ZIP-файла или старого файла .eggfile, и в этом случае ресурс даже не будет физическим файлом в компьютере пользователя.
Было предпринято несколько попыток решить эти проблемы, в том числе с помощью setuptools.pkg_resources . Однако с появлением в стандартной библиотеке Python 3.7 importlib.resources теперь есть один стандартный способ работы с ресурсными файлами.
Представляем importlib.resources
importlib.resources предоставляет доступ к ресурсам внутри пакетов. В этом контексте ресурс — это любой файл, находящийся в импортируемом пакете. Файл может соответствовать, а может и не соответствовать физическому файлу в файловой системе.
Здесь есть несколько преимуществ: при повторном использовании системы импорта получаем более последовательный способ работы с файлами внутри пакетов плюс более лёгкий доступ к ресурсным файлам в других пакетах. Вот что об этом сказано в документации:
Если вы можете импортировать пакет, то можете иметь доступ к ресурсам внутри этого пакета. (Источник.)
importlib.resources стали частью стандартной библиотеки в Python 3.7. А для более старых версий Python имеется бэкпорт importlib_resources . Чтобы задействовать бэкпорт, надо установить его из PyPI:
Бэкпорт совместим с Python 2.7, а также Python 3.4 и более поздними версиями.
При работе с importlib.resources есть одно условие: ресурсные файлы должны быть доступны внутри обычного пакета. Пакеты пространства имён не поддерживаются. На практике это означает, что файл должен находиться в каталоге, содержащем файл __init__.py .
В качестве первого примера предположим, что у нас в пакете есть такие ресурсы:
__init__.py — это просто пустой файл, необходимый для указания на то, что books (книги) — это обычный пакет.
Затем можем использовать open_text() и open_binary() для открытия текстовых и бинарных файлов соответственно:
open_text() и open_binary() эквивалентны встроенному open() с параметром mode , имеющим значения rt и rb соответственно. Также доступны в виде read_text() и read_binary() удобные функции для чтения текстовых или двоичных файлов. Ещё больше узнать можно в официальной документации.
Примечание: чтобы полностью перейти на бэкпорт для старых версий Python, импортируем importlib.resources :
Больше узнать об этом можно в разделе «Полезные советы» этой статьи. Далее в этой части статьи покажем несколько сложных примеров работы с ресурсными файлами на практике.
Файлы данных
В качестве более полного примера работы с файлами данных рассмотрим, как можно реализовать программу викторины, основанную на демографических данных Организации Объединенных Наций. Сначала создаём пакет data и загружаем WPP2019_TotalPopulationBySex.csv с веб-сайта ООН:
Откроем файл CSV и посмотрим на данные:
В каждой строке мы видим данные о населении страны за определённый год и вариант, указывающий на соответствующий прогнозный сценарий. В файле содержатся прогнозы численности населения по странам мира до 2100 года.
Следующая функция считывает этот файл и выдаёт общую численность населения той или иной страны за конкретный year (год) и variant (вариант):
Выделенные жирным шрифтом строки показывают применение importlib.resources для открытия файла данных. Функция возвращает словарь с численностью населения:
Имея такой словарь с данными по численности населения, можно сделать много чего интересного, например анализ и визуализации. Ну а мы создадим игру-викторину, в которой участников просят определить, какая страна в наборе имеет самую большую численность населения. Вот как будет выглядеть эта игра:
Вдаваться в подробности этой реализации не будем, так как они совершенно не имеют отношения к предмету рассмотрения нашей статьи. Однако полный исходный код мы можем показать.
Исходный код демографической викторины:
Демографическая викторина состоит из двух функций: одна считывает данные по численности населения (как мы это делали чуть выше), а другая запускает саму викторину:
Обратите внимание: здесь в строке 24 мы проверяем, что LocID меньше 900 . LocID , равный 900 и выше, указывает не на страновые данные, а на данные по миру, частям света, такие как World , Asia и т.д.
Пример: значки и Tkinter
При создании графических пользовательских интерфейсов (ГПИ) часто требуется включать ресурсные файлы, такие как значки. На следующем примере научимся делать это с помощью importlib.resources . В итоге приложение будет выглядеть довольно просто, но со вкусом благодаря оригинальному значку и оформлению кнопки Goodbye:
В примере используется пакет ГПИ Tkinter, доступный в стандартной библиотеке. Он основан на оконной системе Tk, изначально разработанной для языка программирования Tcl. Существует множество других пакетов ГПИ, доступных для Python. Если вы используете один из них, то должны уметь добавлять значки в своё приложение с помощью идей, подобных тем, что представлены здесь.
В Tkinter изображения обрабатываются классом PhotoImage . Чтобы создать PhotoImage , передаём путь к файлу изображения.
При распространении пакета вовсе не гарантируется, что ресурсные файлы будут существовать в файловой системе как физические файлы. importlib.resources решает эту проблему с помощью path() . Эта функция вернёт путь к ресурсному файлу, создав при необходимости временный файл.
Чтобы убедиться, что все временные файлы очищены правильно, задействуем path() в качестве менеджера контекста, используя ключевое слово with :
Для полного примера предположим, что у нас есть следующая файловая иерархия:
Хотите попробовать пример самостоятельно? Скачайте эти файлы вместе с остальным исходным кодом, приведённым в этой статье, перейдя по ссылке ниже:
Получить исходный код: Нажмите здесь и получите исходный код, используемый для изучения системы импорта Python в этой статье.
Код хранится в файле со специальным именем __main__.py . Это имя указывает на то, файл является точкой входа для пакета. Благодаря файлу __main__.py наш пакет может выполняться с python -m :
ГПИ определяется в классе Hello . Обратите внимание, что для получения пути к файлам изображений используется importlib.resources :
Официальная документация содержит хороший список ресурсов, с которого можно начать изучение. Ещё один отличный ресурс — это «Руководство по TkDocs», которое показывает, как использовать Tk в других языках.
Примечание: единственное, что может вызывать неудобство при работе с изображениями в Tkinter, так это то, что здесь нужно следить за тем, чтобы изображения не удалялись механизмом автоматического управления памятью. Из-за того, как Python и Tk взаимодействуют, сборщик мусора в Python (по крайней мере, в CPython) не регистрирует, что .iconphoto() и Button используют изображения.
Чтобы убедиться, что изображения сохраняются, нужно вручную добавлять ссылку на них. В нашем коде это было сделано в строках 18 и 31.
Динамический импорт
Одна из отличительных особенностей Python в том, что это очень динамичный язык. Можно много чего сделать с программой Python во время её выполнения (хотя иногда делать этого не стоит), например добавлять атрибуты к классу, переопределять методы или изменять строку документации модуля. Мы можем изменить print() так, чтобы он ничего не делал:
На самом деле, мы не переопределяем print() . Мы определяем другой print() , который затеняет встроенный print() . Для возвращения к исходному print() удаляем наш пользовательский print() с помощью del print . Так можно затенить любой объект Python, встроенный в интерпретатор.
Обратите внимание: в приведенном выше примере мы переопределяем print() с помощью лямбда-функции. Также можно было бы использовать определение обычной функции:
В этой части статьи мы ещё узнаем, как выполнять динамический импорт в Python. Освоив его, вы избавитесь от необходимости решать, что импортировать во время выполнения программы.
importlib
До сих пор мы использовали ключевое слово import для явного импорта модулей и пакетов в Python. Однако весь механизм импорта доступен в пакете importlib , что позволяет нам выполнять импорт более динамично. Следующий скрипт запрашивает у пользователя имя модуля, импортирует этот модуль и выводит строку его документации:
import_module() возвращает объект модуля, который можно привязать к любой переменной. После чего мы можем обращаться с этой переменной как с обычным импортируемым модулем. Этот скрипт можно использовать вот так:
В каждом случае модуль импортируется динамически с помощью import_module() .
Фабричный метод с пакетами пространства имён
Вернёмся к примеру с сериализаторами. Благодаря serializers , реализованным в качестве пакета пространства имён, у нас появилась возможность добавлять пользовательские сериализаторы. Сериализаторы создаются с помощью фабрики сериализаторов. Попробуем сделать это, использовав importlib .
Добавим в наш локальный пакет пространства имён serializers следующий код:
Фабрика get_serializer() может создать сериализаторы динамически на основе параметра format , а затем serialize() может применить сериализатор к любому объекту, реализующему метод .serialize() .
Фабрика делает строгие предположения об именовании модуля и класса, которые содержат конкретные сериализаторы. Далее в статье мы узнаем об архитектуре плагинов, которая придаёт больше гибкости.
А пока воссоздадим предыдущий пример вот таким образом:
В этом случае нам больше не нужно выполнять явный импорт каждого сериализатора. Имя сериализатора указываем со строкой. Строка может быть даже выбрана пользователем во время выполнения.
Обратите внимание: в обычном пакете мы бы, наверное, реализовали get_serializer() и serialize() в файле __init__.py . Так мы могли бы просто импортировать serializers , а затем вызвать serializers.serialize() .
Но пакеты пространства имён не могут использовать __init__.py , поэтому нужно реализовать эти функции в отдельном модуле.
Последний пример показывает, что мы получаем соответствующее сообщение об ошибке, если пытаемся сериализоваться в формат, который не был реализован.
Пакет плагинов
Рассмотрим ещё один пример использования динамического импорта. Мы можем использовать следующий модуль для настройки гибкой архитектуры плагинов в коде. Это похоже на то, что было в предыдущем примере, в котором мы могли подключить сериализаторы для различных форматов, добавив новые модули.
Эскпериментальное средство визуализации Glue — это одно из приложений, эффективно использующих плагины. Оно сходу может читать множество различных форматов данных. Если всё-таки нужный формат данных не поддерживается, можно написать собственный пользовательский загрузчик данных.
Просто добавляется функция, которая декорируется и помещается в специальное место, чтобы Glue было легче её найти. И никакую часть исходного кода Glue менять не надо. Все детали смотрите в документации.
Мы можем настроить аналогичную архитектуру плагина для использования в своих проектах. В этой архитектуре два уровня:
- Пакет плагинов — это набор связанных плагинов, соответствующих пакету Python.
- Плагин — это пользовательское поведение, доступное в модуле Python.
Модуль plugins , который предоставляет архитектуру плагина, имеет следующие функции:
Фабричные функции используются для удобного добавления функциональности в пакеты плагинов. Вскоре увидим примеры того, как это происходит.
Рассматривать код во всех деталях не будем: это выходит за рамки статьи. Если вам интересно, можем показать реализацию ниже.
В следующем коде показана реализация plugins.py , описанная выше:
Эта реализация немного упрощена. Так, она не выполняет явной обработки ошибок. Более полная реализация доступна по ссылке на проект PyPlugs.
_import() использует importlib.import_module() для динамической загрузки плагинов. А _import_all() использует importlib.resources.contents() для перечисления всех доступных плагинов в данном пакете.
Рассмотрим несколько примеров использования плагинов. Первый пример — это пакет greeter , который можно использовать для добавления различных приветствий в приложение. Полная архитектура плагинов здесь определённо избыточна, но она показывает, как работают плагины. Представьте, что у вас такой пакет greeter :
Каждый модуль greeter определяет функцию, которая принимает один аргумент name . Посмотрите, как с помощью декоратора @register все они регистрируются в качестве плагинов:
Обратите внимание: для упрощения обнаружения и импорта плагинов имя каждого плагина содержит не имя функции, а имя модуля, в котором он находится. Поэтому на каждый файл может быть только один плагин.
В завершение настройки greeter как пакета плагинов можно использовать фабричные функции в plugins для добавления функциональности в сам пакет greeter :
Теперь мы можем использовать greetings() и greet() вот так:
Заметьте, что greetings() автоматически обнаруживает все плагины, доступные в пакете.
Мы также можем более динамически выбирать, какой плагин вызывать. В следующем примере выбираем плагин случайным образом. Плагин также можно выбрать на основе конфигурационного файла или пользовательских данных:
Для обнаружения и вызова различных плагинов их нужно импортировать. Остановимся ненадолго на том, как plugins работают с импортом. Всё самое главное происходит в следующих двух функциях внутри plugins.py :
_import() внешне кажется простым. Для импорта модуля он использует importlib . Но здесь происходит ещё кое-что:
- Система импорта Python гарантирует, что каждый плагин импортируется только один раз.
- Декораторы @register , определённые внутри каждого модуля plugin, регистрируют каждый импортированный плагин.
- В полной реализации для работы с отсутствующими плагинами будет обработка ошибок.
_import_all() обнаруживает все плагины в пакете. Вот как это работает:
- contents() из importlib.resources выводит список всех файлов внутри пакета.
- Результаты фильтруются для поиска потенциальных плагинов.
- Каждый файл Python, не начинающийся с подчеркивания, импортируется.
- Плагины в любом из файлов обнаруживаются и регистрируются.
Завершим эту часть статьи финальной версией пакета пространства имён. Одной из нерешённых проблем было то, что фабрика get_serializer() делала строгие предположения об именовании классов сериализатора. С помощью плагинов можно сделать их более гибкими.
Первым делом добавляем строку, регистрирующую каждый из сериализаторов. Вот пример того, как это делается в сериализаторе yaml :
Затем обновляем get_serializers() для использования plugins :
Мы реализуем get_serializer() с помощью call_factory() , так как это автоматически инстанцирует каждый сериализатор. При таком рефакторинге сериализаторы работают точно так же, как и раньше. Но теперь у нас больше гибкости в именовании классов сериализаторов.
Ещё больше об использовании плагинов можно узнать в PyPlugs на PyPI и презентации Плагины: добавление гибкости приложениям из PyCon 2019.
5. The import system¶
Python code in one module gains access to the code in another module by the process of importing it. The import statement is the most common way of invoking the import machinery, but it is not the only way. Functions such as importlib.import_module() and built-in __import__() can also be used to invoke the import machinery.
The import statement combines two operations; it searches for the named module, then it binds the results of that search to a name in the local scope. The search operation of the import statement is defined as a call to the __import__() function, with the appropriate arguments. The return value of __import__() is used to perform the name binding operation of the import statement. See the import statement for the exact details of that name binding operation.
A direct call to __import__() performs only the module search and, if found, the module creation operation. While certain side-effects may occur, such as the importing of parent packages, and the updating of various caches (including sys.modules ), only the import statement performs a name binding operation.
When an import statement is executed, the standard builtin __import__() function is called. Other mechanisms for invoking the import system (such as importlib.import_module() ) may choose to bypass __import__() and use their own solutions to implement import semantics.
When a module is first imported, Python searches for the module and if found, it creates a module object 1, initializing it. If the named module cannot be found, a ModuleNotFoundError is raised. Python implements various strategies to search for the named module when the import machinery is invoked. These strategies can be modified and extended by using various hooks described in the sections below.
Changed in version 3.3: The import system has been updated to fully implement the second phase of PEP 302. There is no longer any implicit import machinery — the full import system is exposed through sys.meta_path . In addition, native namespace package support has been implemented (see PEP 420).
5.1. importlib ¶
The importlib module provides a rich API for interacting with the import system. For example importlib.import_module() provides a recommended, simpler API than built-in __import__() for invoking the import machinery. Refer to the importlib library documentation for additional detail.
5.2. Packages¶
Python has only one type of module object, and all modules are of this type, regardless of whether the module is implemented in Python, C, or something else. To help organize modules and provide a naming hierarchy, Python has a concept of packages .
You can think of packages as the directories on a file system and modules as files within directories, but don’t take this analogy too literally since packages and modules need not originate from the file system. For the purposes of this documentation, we’ll use this convenient analogy of directories and files. Like file system directories, packages are organized hierarchically, and packages may themselves contain subpackages, as well as regular modules.
It’s important to keep in mind that all packages are modules, but not all modules are packages. Or put another way, packages are just a special kind of module. Specifically, any module that contains a __path__ attribute is considered a package.
All modules have a name. Subpackage names are separated from their parent package name by a dot, akin to Python’s standard attribute access syntax. Thus you might have a package called email , which in turn has a subpackage called email.mime and a module within that subpackage called email.mime.text .
5.2.1. Regular packages¶
Python defines two types of packages, regular packages and namespace packages . Regular packages are traditional packages as they existed in Python 3.2 and earlier. A regular package is typically implemented as a directory containing an __init__.py file. When a regular package is imported, this __init__.py file is implicitly executed, and the objects it defines are bound to names in the package’s namespace. The __init__.py file can contain the same Python code that any other module can contain, and Python will add some additional attributes to the module when it is imported.
For example, the following file system layout defines a top level parent package with three subpackages:
Importing parent.one will implicitly execute parent/__init__.py and parent/one/__init__.py . Subsequent imports of parent.two or parent.three will execute parent/two/__init__.py and parent/three/__init__.py respectively.
5.2.2. Namespace packages¶
A namespace package is a composite of various portions , where each portion contributes a subpackage to the parent package. Portions may reside in different locations on the file system. Portions may also be found in zip files, on the network, or anywhere else that Python searches during import. Namespace packages may or may not correspond directly to objects on the file system; they may be virtual modules that have no concrete representation.
Namespace packages do not use an ordinary list for their __path__ attribute. They instead use a custom iterable type which will automatically perform a new search for package portions on the next import attempt within that package if the path of their parent package (or sys.path for a top level package) changes.
With namespace packages, there is no parent/__init__.py file. In fact, there may be multiple parent directories found during import search, where each one is provided by a different portion. Thus parent/one may not be physically located next to parent/two . In this case, Python will create a namespace package for the top-level parent package whenever it or one of its subpackages is imported.
See also PEP 420 for the namespace package specification.
5.3. Searching¶
To begin the search, Python needs the fully qualified name of the module (or package, but for the purposes of this discussion, the difference is immaterial) being imported. This name may come from various arguments to the import statement, or from the parameters to the importlib.import_module() or __import__() functions.
This name will be used in various phases of the import search, and it may be the dotted path to a submodule, e.g. foo.bar.baz . In this case, Python first tries to import foo , then foo.bar , and finally foo.bar.baz . If any of the intermediate imports fail, a ModuleNotFoundError is raised.
5.3.1. The module cache¶
The first place checked during import search is sys.modules . This mapping serves as a cache of all modules that have been previously imported, including the intermediate paths. So if foo.bar.baz was previously imported, sys.modules will contain entries for foo , foo.bar , and foo.bar.baz . Each key will have as its value the corresponding module object.
During import, the module name is looked up in sys.modules and if present, the associated value is the module satisfying the import, and the process completes. However, if the value is None , then a ModuleNotFoundError is raised. If the module name is missing, Python will continue searching for the module.
sys.modules is writable. Deleting a key may not destroy the associated module (as other modules may hold references to it), but it will invalidate the cache entry for the named module, causing Python to search anew for the named module upon its next import. The key can also be assigned to None , forcing the next import of the module to result in a ModuleNotFoundError .
Beware though, as if you keep a reference to the module object, invalidate its cache entry in sys.modules , and then re-import the named module, the two module objects will not be the same. By contrast, importlib.reload() will reuse the same module object, and simply reinitialise the module contents by rerunning the module’s code.
5.3.2. Finders and loaders¶
If the named module is not found in sys.modules , then Python’s import protocol is invoked to find and load the module. This protocol consists of two conceptual objects, finders and loaders . A finder’s job is to determine whether it can find the named module using whatever strategy it knows about. Objects that implement both of these interfaces are referred to as importers — they return themselves when they find that they can load the requested module.
Python includes a number of default finders and importers. The first one knows how to locate built-in modules, and the second knows how to locate frozen modules. A third default finder searches an import path for modules. The import path is a list of locations that may name file system paths or zip files. It can also be extended to search for any locatable resource, such as those identified by URLs.
The import machinery is extensible, so new finders can be added to extend the range and scope of module searching.
Finders do not actually load modules. If they can find the named module, they return a module spec, an encapsulation of the module’s import-related information, which the import machinery then uses when loading the module.
The following sections describe the protocol for finders and loaders in more detail, including how you can create and register new ones to extend the import machinery.
Changed in version 3.4: In previous versions of Python, finders returned loaders directly, whereas now they return module specs which contain loaders. Loaders are still used during import but have fewer responsibilities.
5.3.3. Import hooks¶
The import machinery is designed to be extensible; the primary mechanism for this are the import hooks. There are two types of import hooks: meta hooks and import path hooks.
Meta hooks are called at the start of import processing, before any other import processing has occurred, other than sys.modules cache look up. This allows meta hooks to override sys.path processing, frozen modules, or even built-in modules. Meta hooks are registered by adding new finder objects to sys.meta_path , as described below.
Import path hooks are called as part of sys.path (or package.__path__ ) processing, at the point where their associated path item is encountered. Import path hooks are registered by adding new callables to sys.path_hooks as described below.
5.3.4. The meta path¶
When the named module is not found in sys.modules , Python next searches sys.meta_path , which contains a list of meta path finder objects. These finders are queried in order to see if they know how to handle the named module. Meta path finders must implement a method called find_spec() which takes three arguments: a name, an import path, and (optionally) a target module. The meta path finder can use any strategy it wants to determine whether it can handle the named module or not.
If the meta path finder knows how to handle the named module, it returns a spec object. If it cannot handle the named module, it returns None . If sys.meta_path processing reaches the end of its list without returning a spec, then a ModuleNotFoundError is raised. Any other exceptions raised are simply propagated up, aborting the import process.
The find_spec() method of meta path finders is called with two or three arguments. The first is the fully qualified name of the module being imported, for example foo.bar.baz . The second argument is the path entries to use for the module search. For top-level modules, the second argument is None , but for submodules or subpackages, the second argument is the value of the parent package’s __path__ attribute. If the appropriate __path__ attribute cannot be accessed, a ModuleNotFoundError is raised. The third argument is an existing module object that will be the target of loading later. The import system passes in a target module only during reload.
The meta path may be traversed multiple times for a single import request. For example, assuming none of the modules involved has already been cached, importing foo.bar.baz will first perform a top level import, calling mpf.find_spec("foo", None, None) on each meta path finder ( mpf ). After foo has been imported, foo.bar will be imported by traversing the meta path a second time, calling mpf.find_spec("foo.bar", foo.__path__, None) . Once foo.bar has been imported, the final traversal will call mpf.find_spec("foo.bar.baz", foo.bar.__path__, None) .
Some meta path finders only support top level imports. These importers will always return None when anything other than None is passed as the second argument.
Python’s default sys.meta_path has three meta path finders, one that knows how to import built-in modules, one that knows how to import frozen modules, and one that knows how to import modules from an import path (i.e. the path based finder ).
Changed in version 3.4: The find_spec() method of meta path finders replaced find_module() , which is now deprecated. While it will continue to work without change, the import machinery will try it only if the finder does not implement find_spec() .
Changed in version 3.10: Use of find_module() by the import system now raises ImportWarning .
5.4. Loading¶
If and when a module spec is found, the import machinery will use it (and the loader it contains) when loading the module. Here is an approximation of what happens during the loading portion of import:
Note the following details:
-
If there is an existing module object with the given name in sys.modules , import will have already returned it.
-
The module will exist in sys.modules before the loader executes the module code. This is crucial because the module code may (directly or indirectly) import itself; adding it to sys.modules beforehand prevents unbounded recursion in the worst case and multiple loading in the best.
-
If loading fails, the failing module – and only the failing module – gets removed from sys.modules . Any module already in the sys.modules cache, and any module that was successfully loaded as a side-effect, must remain in the cache. This contrasts with reloading where even the failing module is left in sys.modules .
-
After the module is created but before execution, the import machinery sets the import-related module attributes (“_init_module_attrs” in the pseudo-code example above), as summarized in a later section .
-
Module execution is the key moment of loading in which the module’s namespace gets populated. Execution is entirely delegated to the loader, which gets to decide what gets populated and how.
-
The module created during loading and passed to exec_module() may not be the one returned at the end of import 2.
Changed in version 3.4: The import system has taken over the boilerplate responsibilities of loaders. These were previously performed by the importlib.abc.Loader.load_module() method.
5.4.1. Loaders¶
Module loaders provide the critical function of loading: module execution. The import machinery calls the importlib.abc.Loader.exec_module() method with a single argument, the module object to execute. Any value returned from exec_module() is ignored.
Loaders must satisfy the following requirements:
-
If the module is a Python module (as opposed to a built-in module or a dynamically loaded extension), the loader should execute the module’s code in the module’s global name space ( module.__dict__ ).
-
If the loader cannot execute the module, it should raise an ImportError , although any other exception raised during exec_module() will be propagated.
In many cases, the finder and loader can be the same object; in such cases the find_spec() method would just return a spec with the loader set to self .
Module loaders may opt in to creating the module object during loading by implementing a create_module() method. It takes one argument, the module spec, and returns the new module object to use during loading. create_module() does not need to set any attributes on the module object. If the method returns None , the import machinery will create the new module itself.
New in version 3.4: The create_module() method of loaders.
Changed in version 3.4: The load_module() method was replaced by exec_module() and the import machinery assumed all the boilerplate responsibilities of loading.
For compatibility with existing loaders, the import machinery will use the load_module() method of loaders if it exists and the loader does not also implement exec_module() . However, load_module() has been deprecated and loaders should implement exec_module() instead.
The load_module() method must implement all the boilerplate loading functionality described above in addition to executing the module. All the same constraints apply, with some additional clarification:
-
If there is an existing module object with the given name in sys.modules , the loader must use that existing module. (Otherwise, importlib.reload() will not work correctly.) If the named module does not exist in sys.modules , the loader must create a new module object and add it to sys.modules .
-
The module must exist in sys.modules before the loader executes the module code, to prevent unbounded recursion or multiple loading.
-
If loading fails, the loader must remove any modules it has inserted into sys.modules , but it must remove only the failing module(s), and only if the loader itself has loaded the module(s) explicitly.
Changed in version 3.5: A DeprecationWarning is raised when exec_module() is defined but create_module() is not.
Changed in version 3.6: An ImportError is raised when exec_module() is defined but create_module() is not.
Changed in version 3.10: Use of load_module() will raise ImportWarning .
5.4.2. Submodules¶
When a submodule is loaded using any mechanism (e.g. importlib APIs, the import or import-from statements, or built-in __import__() ) a binding is placed in the parent module’s namespace to the submodule object. For example, if package spam has a submodule foo , after importing spam.foo , spam will have an attribute foo which is bound to the submodule. Let’s say you have the following directory structure:
and spam/__init__.py has the following line in it:
then executing the following puts name bindings for foo and Foo in the spam module:
Given Python’s familiar name binding rules this might seem surprising, but it’s actually a fundamental feature of the import system. The invariant holding is that if you have sys.modules[‘spam’] and sys.modules[‘spam.foo’] (as you would after the above import), the latter must appear as the foo attribute of the former.
5.4.3. Module spec¶
The import machinery uses a variety of information about each module during import, especially before loading. Most of the information is common to all modules. The purpose of a module’s spec is to encapsulate this import-related information on a per-module basis.
Using a spec during import allows state to be transferred between import system components, e.g. between the finder that creates the module spec and the loader that executes it. Most importantly, it allows the import machinery to perform the boilerplate operations of loading, whereas without a module spec the loader had that responsibility.
The module’s spec is exposed as the __spec__ attribute on a module object. See ModuleSpec for details on the contents of the module spec.
New in version 3.4.
5.4.4. Import-related module attributes¶
The import machinery fills in these attributes on each module object during loading, based on the module’s spec, before the loader executes the module.
The __name__ attribute must be set to the fully qualified name of the module. This name is used to uniquely identify the module in the import system.
The __loader__ attribute must be set to the loader object that the import machinery used when loading the module. This is mostly for introspection, but can be used for additional loader-specific functionality, for example getting data associated with a loader.
The module’s __package__ attribute must be set. Its value must be a string, but it can be the same value as its __name__ . When the module is a package, its __package__ value should be set to its __name__ . When the module is not a package, __package__ should be set to the empty string for top-level modules, or for submodules, to the parent package’s name. See PEP 366 for further details.
This attribute is used instead of __name__ to calculate explicit relative imports for main modules, as defined in PEP 366. It is expected to have the same value as __spec__.parent .
Changed in version 3.6: The value of __package__ is expected to be the same as __spec__.parent .
The __spec__ attribute must be set to the module spec that was used when importing the module. Setting __spec__ appropriately applies equally to modules initialized during interpreter startup . The one exception is __main__ , where __spec__ is set to None in some cases .
When __package__ is not defined, __spec__.parent is used as a fallback.
New in version 3.4.
Changed in version 3.6: __spec__.parent is used as a fallback when __package__ is not defined.
If the module is a package (either regular or namespace), the module object’s __path__ attribute must be set. The value must be iterable, but may be empty if __path__ has no further significance. If __path__ is not empty, it must produce strings when iterated over. More details on the semantics of __path__ are given below .
Non-package modules should not have a __path__ attribute.
__file__ is optional (if set, value must be a string). It indicates the pathname of the file from which the module was loaded (if loaded from a file), or the pathname of the shared library file for extension modules loaded dynamically from a shared library. It might be missing for certain types of modules, such as C modules that are statically linked into the interpreter, and the import system may opt to leave it unset if it has no semantic meaning (e.g. a module loaded from a database).
If __file__ is set then the __cached__ attribute might also be set, which is the path to any compiled version of the code (e.g. byte-compiled file). The file does not need to exist to set this attribute; the path can simply point to where the compiled file would exist (see PEP 3147).
Note that __cached__ may be set even if __file__ is not set. However, that scenario is quite atypical. Ultimately, the loader is what makes use of the module spec provided by the finder (from which __file__ and __cached__ are derived). So if a loader can load from a cached module but otherwise does not load from a file, that atypical scenario may be appropriate.
5.4.5. module.__path__¶
By definition, if a module has a __path__ attribute, it is a package.
A package’s __path__ attribute is used during imports of its subpackages. Within the import machinery, it functions much the same as sys.path , i.e. providing a list of locations to search for modules during import. However, __path__ is typically much more constrained than sys.path .
__path__ must be an iterable of strings, but it may be empty. The same rules used for sys.path also apply to a package’s __path__ , and sys.path_hooks (described below) are consulted when traversing a package’s __path__ .
A package’s __init__.py file may set or alter the package’s __path__ attribute, and this was typically the way namespace packages were implemented prior to PEP 420. With the adoption of PEP 420, namespace packages no longer need to supply __init__.py files containing only __path__ manipulation code; the import machinery automatically sets __path__ correctly for the namespace package.
5.4.6. Module reprs¶
By default, all modules have a usable repr, however depending on the attributes set above, and in the module’s spec, you can more explicitly control the repr of module objects.
If the module has a spec ( __spec__ ), the import machinery will try to generate a repr from it. If that fails or there is no spec, the import system will craft a default repr using whatever information is available on the module. It will try to use the module.__name__ , module.__file__ , and module.__loader__ as input into the repr, with defaults for whatever information is missing.
Here are the exact rules used:
-
If the module has a __spec__ attribute, the information in the spec is used to generate the repr. The “name”, “loader”, “origin”, and “has_location” attributes are consulted.
-
If the module has a __file__ attribute, this is used as part of the module’s repr.
-
If the module has no __file__ but does have a __loader__ that is not None , then the loader’s repr is used as part of the module’s repr.
-
Otherwise, just use the module’s __name__ in the repr.
Changed in version 3.4: Use of loader.module_repr() has been deprecated and the module spec is now used by the import machinery to generate a module repr.
For backward compatibility with Python 3.3, the module repr will be generated by calling the loader’s module_repr() method, if defined, before trying either approach described above. However, the method is deprecated.
Changed in version 3.10: Calling module_repr() now occurs after trying to use a module’s __spec__ attribute but before falling back on __file__ . Use of module_repr() is slated to stop in Python 3.12.
5.4.7. Cached bytecode invalidation¶
Before Python loads cached bytecode from a .pyc file, it checks whether the cache is up-to-date with the source .py file. By default, Python does this by storing the source’s last-modified timestamp and size in the cache file when writing it. At runtime, the import system then validates the cache file by checking the stored metadata in the cache file against the source’s metadata.
Python also supports “hash-based” cache files, which store a hash of the source file’s contents rather than its metadata. There are two variants of hash-based .pyc files: checked and unchecked. For checked hash-based .pyc files, Python validates the cache file by hashing the source file and comparing the resulting hash with the hash in the cache file. If a checked hash-based cache file is found to be invalid, Python regenerates it and writes a new checked hash-based cache file. For unchecked hash-based .pyc files, Python simply assumes the cache file is valid if it exists. Hash-based .pyc files validation behavior may be overridden with the —check-hash-based-pycs flag.
Changed in version 3.7: Added hash-based .pyc files. Previously, Python only supported timestamp-based invalidation of bytecode caches.
5.5. The Path Based Finder¶
As mentioned previously, Python comes with several default meta path finders. One of these, called the path based finder ( PathFinder ), searches an import path , which contains a list of path entries . Each path entry names a location to search for modules.
The path based finder itself doesn’t know how to import anything. Instead, it traverses the individual path entries, associating each of them with a path entry finder that knows how to handle that particular kind of path.
The default set of path entry finders implement all the semantics for finding modules on the file system, handling special file types such as Python source code ( .py files), Python byte code ( .pyc files) and shared libraries (e.g. .so files). When supported by the zipimport module in the standard library, the default path entry finders also handle loading all of these file types (other than shared libraries) from zipfiles.
Path entries need not be limited to file system locations. They can refer to URLs, database queries, or any other location that can be specified as a string.
The path based finder provides additional hooks and protocols so that you can extend and customize the types of searchable path entries. For example, if you wanted to support path entries as network URLs, you could write a hook that implements HTTP semantics to find modules on the web. This hook (a callable) would return a path entry finder supporting the protocol described below, which was then used to get a loader for the module from the web.
A word of warning: this section and the previous both use the term finder, distinguishing between them by using the terms meta path finder and path entry finder . These two types of finders are very similar, support similar protocols, and function in similar ways during the import process, but it’s important to keep in mind that they are subtly different. In particular, meta path finders operate at the beginning of the import process, as keyed off the sys.meta_path traversal.
By contrast, path entry finders are in a sense an implementation detail of the path based finder, and in fact, if the path based finder were to be removed from sys.meta_path , none of the path entry finder semantics would be invoked.
5.5.1. Path entry finders¶
The path based finder is responsible for finding and loading Python modules and packages whose location is specified with a string path entry . Most path entries name locations in the file system, but they need not be limited to this.
As a meta path finder, the path based finder implements the find_spec() protocol previously described, however it exposes additional hooks that can be used to customize how modules are found and loaded from the import path .
Three variables are used by the path based finder , sys.path , sys.path_hooks and sys.path_importer_cache . The __path__ attributes on package objects are also used. These provide additional ways that the import machinery can be customized.
sys.path contains a list of strings providing search locations for modules and packages. It is initialized from the PYTHONPATH environment variable and various other installation- and implementation-specific defaults. Entries in sys.path can name directories on the file system, zip files, and potentially other “locations” (see the site module) that should be searched for modules, such as URLs, or database queries. Only strings should be present on sys.path ; all other data types are ignored.
The path based finder is a meta path finder , so the import machinery begins the import path search by calling the path based finder’s find_spec() method as described previously. When the path argument to find_spec() is given, it will be a list of string paths to traverse — typically a package’s __path__ attribute for an import within that package. If the path argument is None , this indicates a top level import and sys.path is used.
The path based finder iterates over every entry in the search path, and for each of these, looks for an appropriate path entry finder ( PathEntryFinder ) for the path entry. Because this can be an expensive operation (e.g. there may be stat() call overheads for this search), the path based finder maintains a cache mapping path entries to path entry finders. This cache is maintained in sys.path_importer_cache (despite the name, this cache actually stores finder objects rather than being limited to importer objects). In this way, the expensive search for a particular path entry location’s path entry finder need only be done once. User code is free to remove cache entries from sys.path_importer_cache forcing the path based finder to perform the path entry search again 3.
If the path entry is not present in the cache, the path based finder iterates over every callable in sys.path_hooks . Each of the path entry hooks in this list is called with a single argument, the path entry to be searched. This callable may either return a path entry finder that can handle the path entry, or it may raise ImportError . An ImportError is used by the path based finder to signal that the hook cannot find a path entry finder for that path entry . The exception is ignored and import path iteration continues. The hook should expect either a string or bytes object; the encoding of bytes objects is up to the hook (e.g. it may be a file system encoding, UTF-8, or something else), and if the hook cannot decode the argument, it should raise ImportError .
If sys.path_hooks iteration ends with no path entry finder being returned, then the path based finder’s find_spec() method will store None in sys.path_importer_cache (to indicate that there is no finder for this path entry) and return None , indicating that this meta path finder could not find the module.
If a path entry finder is returned by one of the path entry hook callables on sys.path_hooks , then the following protocol is used to ask the finder for a module spec, which is then used when loading the module.
The current working directory – denoted by an empty string – is handled slightly differently from other entries on sys.path . First, if the current working directory is found to not exist, no value is stored in sys.path_importer_cache . Second, the value for the current working directory is looked up fresh for each module lookup. Third, the path used for sys.path_importer_cache and returned by importlib.machinery.PathFinder.find_spec() will be the actual current working directory and not the empty string.
5.5.2. Path entry finder protocol¶
In order to support imports of modules and initialized packages and also to contribute portions to namespace packages, path entry finders must implement the find_spec() method.
find_spec() takes two arguments: the fully qualified name of the module being imported, and the (optional) target module. find_spec() returns a fully populated spec for the module. This spec will always have “loader” set (with one exception).
To indicate to the import machinery that the spec represents a namespace portion , the path entry finder sets “submodule_search_locations” to a list containing the portion.
Changed in version 3.4: find_spec() replaced find_loader() and find_module() , both of which are now deprecated, but will be used if find_spec() is not defined.
Older path entry finders may implement one of these two deprecated methods instead of find_spec() . The methods are still respected for the sake of backward compatibility. However, if find_spec() is implemented on the path entry finder, the legacy methods are ignored.
find_loader() takes one argument, the fully qualified name of the module being imported. find_loader() returns a 2-tuple where the first item is the loader and the second item is a namespace portion .
For backwards compatibility with other implementations of the import protocol, many path entry finders also support the same, traditional find_module() method that meta path finders support. However path entry finder find_module() methods are never called with a path argument (they are expected to record the appropriate path information from the initial call to the path hook).
The find_module() method on path entry finders is deprecated, as it does not allow the path entry finder to contribute portions to namespace packages. If both find_loader() and find_module() exist on a path entry finder, the import system will always call find_loader() in preference to find_module() .
Changed in version 3.10: Calls to find_module() and find_loader() by the import system will raise ImportWarning .
5.6. Replacing the standard import system¶
The most reliable mechanism for replacing the entire import system is to delete the default contents of sys.meta_path , replacing them entirely with a custom meta path hook.
If it is acceptable to only alter the behaviour of import statements without affecting other APIs that access the import system, then replacing the builtin __import__() function may be sufficient. This technique may also be employed at the module level to only alter the behaviour of import statements within that module.
To selectively prevent the import of some modules from a hook early on the meta path (rather than disabling the standard import system entirely), it is sufficient to raise ModuleNotFoundError directly from find_spec() instead of returning None . The latter indicates that the meta path search should continue, while raising an exception terminates it immediately.
5.7. Package Relative Imports¶
Relative imports use leading dots. A single leading dot indicates a relative import, starting with the current package. Two or more leading dots indicate a relative import to the parent(s) of the current package, one level per dot after the first. For example, given the following package layout:
In either subpackage1/moduleX.py or subpackage1/__init__.py , the following are valid relative imports:
Absolute imports may use either the import <> or from <> import <> syntax, but relative imports may only use the second form; the reason for this is that:
should expose XXX.YYY.ZZZ as a usable expression, but .moduleY is not a valid expression.
5.8. Special considerations for __main__¶
The __main__ module is a special case relative to Python’s import system. As noted elsewhere , the __main__ module is directly initialized at interpreter startup, much like sys and builtins . However, unlike those two, it doesn’t strictly qualify as a built-in module. This is because the manner in which __main__ is initialized depends on the flags and other options with which the interpreter is invoked.
5.8.1. __main__.__spec__¶
Depending on how __main__ is initialized, __main__.__spec__ gets set appropriately or to None .
When Python is started with the -m option, __spec__ is set to the module spec of the corresponding module or package. __spec__ is also populated when the __main__ module is loaded as part of executing a directory, zipfile or other sys.path entry.
In the remaining cases __main__.__spec__ is set to None , as the code used to populate the __main__ does not correspond directly with an importable module:
running from stdin
running directly from a source or bytecode file
Note that __main__.__spec__ is always None in the last case, even if the file could technically be imported directly as a module instead. Use the -m switch if valid module metadata is desired in __main__ .
Note also that even when __main__ corresponds with an importable module and __main__.__spec__ is set accordingly, they’re still considered distinct modules. This is due to the fact that blocks guarded by if __name__ == "__main__": checks only execute when the module is used to populate the __main__ namespace, and not during normal import.
5.9. References¶
The import machinery has evolved considerably since Python’s early days. The original specification for packages is still available to read, although some details have changed since the writing of that document.
The original specification for sys.meta_path was PEP 302, with subsequent extension in PEP 420.
PEP 420 introduced namespace packages for Python 3.3. PEP 420 also introduced the find_loader() protocol as an alternative to find_module() .
PEP 366 describes the addition of the __package__ attribute for explicit relative imports in main modules.
PEP 328 introduced absolute and explicit relative imports and initially proposed __name__ for semantics PEP 366 would eventually specify for __package__ .
PEP 338 defines executing modules as scripts.
PEP 451 adds the encapsulation of per-module import state in spec objects. It also off-loads most of the boilerplate responsibilities of loaders back onto the import machinery. These changes allow the deprecation of several APIs in the import system and also addition of new methods to finders and loaders.