Пишите лучше структурированные модули и пакеты
Если вы какое-то время работали с Python, вы, вероятно, встречали __main__
идиому. Он состоит из пары строк кода, которые обычно выглядят так:
В этой статье я хотел бы более подробно изучить значение этих строк и использовать этот общий шаблон в качестве отправной точки для исследования механизма импорта Python. Это должно помочь вам лучше понять, что происходит во время импорта, а также помочь вам внести структуру в ваши собственные модули и пакеты. (Эта статья написана со ссылкой на стандартную реализацию CPython и версию Python 3.6.)
Выполнение модуля с помощью переводчика
Когда модуль, подобный показанному выше (module_a.py
), передается интерпретатору (например, как python module_a.py
) в командной строке, механизм импорта Python собирает информацию о модуле, определяет и устанавливает несколько атрибутов, которые можно использовать для управления поведением модуля. . Эти атрибуты устанавливаются перед выполнением любого кода в модуле и доступны изнутри модуля. Список этих атрибутов можно найти здесь.
Еще одна вещь, которая происходит, когда интерпретатор вызывается с файлом, - это то, что модуль __main__
инициализируется, а операторы в файле выполняются и становятся частью пространства имен модуля. Для атрибута __name__
модуля __main__
установлено строковое значение __main__
. Подробнее об этом ниже.
Чтобы лучше понять приведенные выше абзацы, давайте добавим несколько операторов в module_a.py
, чтобы проверить атрибуты, установленные интерпретатором Python:
globals()
- это встроенная функция, которая возвращает словарь, содержащий все символы (переменные, методы и т. Д.), Определенные в текущем пространстве имен. Строка 4 в приведенном выше коде копирует словарь, возвращаемый globals()
, перед его повторением и печатью его ключей и значений. (Необходимо работать с копией, потому что переменные k
и v
становятся частью пространства имен для этого модуля и изменяются на каждом шаге. Строки 4–5 предназначены только для того, чтобы распечатать переменные в более удобочитаемом виде, вы можете хорошо замените их на print(globals())
или аналогичное утверждение.)
Когда вы выполняете этот код через:
$ python module_a.py
вы должны увидеть результат, очень похожий на следующий (слегка усеченный для удобства чтения):
$ python module_a.py globals Module A: '__name__': '__main__' '__doc__': 'Module A' '__package__': None '__loader__': <_frozen_importlib_external.SourceFileLoader ...> '__spec__': None '__annotations__': {} '__builtins__': <module 'builtins' (built-in)> '__file__': 'module_a.py' '__cached__': None Hello World
Как видите, большинство атрибутов, описанных в официальной документации, определены и имеют присвоенные им значения. Вы можете видеть, что __file__
содержит имя файла (обычно это относительный или полный путь к файлу), __doc__
содержит строку документации модуля, а __name__
устанавливается равным строке __main__
. Эти атрибуты теперь определены в пространстве имен модуля __main__
, и к ним можно получить прямой доступ, как это сделано в строке 15 приведенного выше кода (module_a
). Поскольку значение __name__
в этом случае __main__
, вызывается функция main()
, которая, в свою очередь, вызывает function_a()
, который выводит Hello World
.
Только условно метод, вызываемый после __name__
проверки, называется main
. Что должно происходить в этом месте - решать автору сценария. Можно вызвать любую (определенную) функцию, метод и т. Д. Или иметь более сложный код инициализации. Подробнее об этом позже.
Импорт модуля
Если мы хотим использовать функции, классы и т. Д., Которые определены в module_a
в другом модуле, скажем в module_b
, мы можем легко импортировать туда module_a
. Предполагая, что оба файла находятся в одном каталоге, код может выглядеть просто так:
Если вы сейчас передадите интерпретатору module_b
, вы увидите следующий результат (усеченный для удобства чтения):
$ python module_b.py globals Module A: '__name__': 'module_a' '__doc__': 'Module A' '__package__': '' '__loader__': <_frozen_importlib_external.SourceFileLoader ...> '__spec__': ModuleSpec(name='module_a', loader=<_frozen_importlib_external.SourceFileLoader ...>, origin='/path/to/module_a.py') '__file__': '/path/to/module_a.py' '__cached__': '/path/to/__pycache__/module_a.cpython-36.pyc' '__builtins__': {'__name__': 'builtins', ...}
Вывод генерируется операторами печати внутри module_a.py
после того, как он был импортирован module_b.py
.
Обратите внимание на различия между этим и первым выводом: __name__
теперь установлен на имя модуля, а не на __main__
, __file__
- это абсолютный путь к файлу, из которого был импортирован модуль, __spec__
установлен на экземпляр ModuleSpec
(см. Здесь для получения дополнительной информации), а __builtins__
устанавливается в словарь модуля builtins
(это деталь реализации CPython).
Также обратите внимание, что Hello World
не распечатывается. Поскольку для module_a
__name__
теперь установлено его имя (а не __main__
), строка 15 в module_a.py
предотвращает вызов main()
при загрузке и импорте модуля, и поэтому function_a()
никогда не вызывается.
Чтобы лучше понять переменные в пространстве имен module_b
, давайте добавим распечатку в module_b.py
:
Запуск этого должен привести к выводу, подобному следующему:
$ python module_b.py globals Module A: '__name__': 'module_a' '__doc__': 'Module A' '__package__': '' '__loader__': <_frozen_importlib_external.SourceFileLoader ...> '__spec__': ModuleSpec(name='module_a', loader=<_frozen_importlib_external.SourceFileLoader ...>, origin='/path/to/module_a.py') '__file__': '/path/to/module_a.py' '__cached__': '/path/to/__pycache__/module_a.cpython-36.pyc' '__builtins__': {'__name__': 'builtins', ...} globals module B: '__name__': '__main__' '__doc__': 'Module B' '__package__': None '__loader__': <_frozen_importlib_external.SourceFileLoader ...> '__spec__': None '__annotations__': {} '__builtins__': <module 'builtins' (built-in)> '__file__': 'module_b.py' '__cached__': None 'module_a': <module 'module_a' from '/path/to/module_a.py'>
Первая половина должна выглядеть так же, как и раньше, а вторая половина должна выглядеть так же, как при запуске python module_a.py
, но с заменой module_a
на module_b
в большинстве мест. Кроме того, пространство имен теперь также содержит module_a
, что делает его (и все, что в нем определено) доступным внутри module_b
.
Еще одна вещь, на которую следует обратить внимание, - это то, что __package__
в пространстве имен module_a
установлен на пустую строку, а __package__
в пространстве имен module_b
установлен на None
. Python попытается определить, является ли модуль частью пакета. Поскольку module_a
в этом случае импортируется module_b
, по крайней мере возможно, что он может быть частью пакета, поэтому для переменной устанавливается пустая строка, а module_b
выполняется напрямую, что означает, что он не может быть частью пакета ( в этом конкретном исполнении).
Вывод показывает нам, что module_a
был успешно импортирован в module_b
, определения его функций загружены и доступны, например:
который будет распечатывать Hello World
(и переменные внутри пространства имен module_a
при импорте).
Модуль __main__
Как упоминалось выше, когда модуль выполняется путем прямого вызова интерпретатора, __main__
модуль инициализируется, чтобы предоставить пространство имен для среды верхнего уровня программы. Чтобы лучше понять, что это означает, мы можем использовать модуль Python sys
для получения списка загруженных модулей (sys
- один из немногих модулей, который инициализируется при запуске интерпретатора). Для этого создадим новый модуль со следующим содержанием:
Переменная sys.modules
содержит словарь со всеми модулями, которые уже были загружены (но не обязательно импортированы). Сортировка для удобства и их распечатка дает примерно следующее (усечено для удобства чтения):
$ python module_c.py modules '__main__': <module 'module_c' from '/path/to/module_c.py'> '_bootlocale': <module '_bootlocale' from '/usr/lib/python3.6/_bootlocale.py'> '_codecs': <module '_codecs' (built-in)> ... ... ... 'warnings': <module 'warnings' from '/usr/lib/python3.6/warnings.py'> 'weakref': <module 'weakref' from '/usr/lib/python3.6/weakref.py'> 'zipimport': <module 'zipimport' (built-in)>
Это сопоставление между именами модулей (с помощью которых можно получить доступ к модулям) и экземплярами модулей (модуль - это сам объект Python) для всех загруженных модулей. Другими словами, перечисленные модули известны интерпретатору и могут быть import
’использованы внутри данного модуля. Это также первое место, где Python будет искать модули для импорта.
Как видите, первая запись - это модуль __main__
, который был инициализирован содержимым module_c
. Это означает, что мы можем использовать этот модуль, чтобы еще больше убедить себя, что модуль __main__
и пространство имен текущего модуля - это одно и то же. Для этого создадим еще один модуль со следующим содержанием:
Запуск этого должен дать:
$ python module_d.py True False True True
Это показывает нам, что не только значение __name__
одинаково в обоих случаях, но они также относятся к одному и тому же объекту в памяти.
Понимание __main__.py
В дополнение к «идиоме __main__
» Python предлагает способ достижения того же эффекта путем создания файла с именем __main__.py
внутри каталога проекта вместе с собственно файлами модуля. Это может быть полезно, когда проект стал очень большим и вы хотите разделить логику на несколько файлов / модулей или если вы хотите сохранить функциональность строго разделенной.
Представьте себе пакет со следующей структурой каталогов:
my_package/ ├── __main__.py ├── module_x.py └── module_y.py
и файлы следующего содержания:
Теперь можно передать каталог интерпретатору Python для выполнения, который выдает следующий результат (усеченный для удобства чтения):
$ python my_package globals Module X: '__name__': 'module_x' '__doc__': 'Module X' '__package__': '' '__loader__': <_frozen_importlib_external.SourceFileLoader ...> '__spec__': ModuleSpec(name='module_x', loader=<_frozen_importlib_external.SourceFileLoader ...>, origin='my_package/module_x.py') '__file__': 'my_package/module_x.py' '__cached__': 'my_package/__pycache__/module_x.cpython-36.pyc' '__builtins__': {'__name__': 'builtins', ...} globals Module Y: '__name__': 'module_y' '__doc__': 'Module Y' '__package__': '' '__loader__': <_frozen_importlib_external.SourceFileLoader ...> '__spec__': ModuleSpec(name='module_y', loader=<_frozen_importlib_external.SourceFileLoader ...>, origin='my_package/module_y.py') '__file__': 'my_package/module_y.py' '__cached__': 'my_package/__pycache__/module_y.cpython-36.pyc' '__builtins__': {'__name__': ...} globals main: '__name__': '__main__' '__doc__': 'Main module' '__package__': '' '__loader__': <_frozen_importlib_external.SourceFileLoader ...> '__spec__': ModuleSpec(name='__main__', loader=<_frozen_importlib_external.SourceFileLoader ...>, origin='my_package/__main__.py') '__annotations__': {} '__builtins__': <module 'builtins' (built-in)> '__file__': 'my_package/__main__.py' '__cached__': 'my_package/__pycache__/__main__.cpython-36.pyc' 'module_x': <module 'module_x' from 'my_package/module_x.py'> 'module_y': <module 'module_y' from 'my_package/module_y.py'> function x function y
Большая часть вывода аналогична тому, что было описано выше, но тот факт, что он вообще печатается, и порядок, в котором он печатается, дает нам представление о том, что делает интерпретатор Python.
Мы видим переменные пространства имен module_x
, за которыми следуют переменные пространства имен module_y
и __main__
модуля. Поскольку модуль __main__
- единственное место, где мы до сих пор выполняли импорт, это говорит нам о том, что интерпретатор автоматически выбирает все, что находится в __main__.py
, и выполняет его, как если бы это было указано в командной строке напрямую (это не совсем так, поскольку пути в большинстве случаев будут абсолютными, а не относительными).
Следующее, на что следует обратить внимание, это то, что атрибут name __name__
для module_x
и module_y
установлен на соответствующие имена, как и следовало ожидать для импортируемых модулей, в то время как __name__
установлен на __main__
для __main__.py
.
Обратите внимание, как module_x
и module_y
являются частью пространства имен в __main__.py
(как и следовало ожидать, поскольку мы импортируем их), и мы можем выполнять вызовы функций, определенных внутри этих модулей.
Последние две строки показывают нам, что также выполняются два вызова функций, определенных в module_x
и module_y
.
Имейте в виду, что каждая строка в каждом модуле автоматически выполняется при импорте (определения функций внутри module_x
и module_y
являются операторами, которые также выполняются, а сами функции - нет).
Также можно передать абсолютный путь к пакету, то есть:
$ python /path/to/my_package
Результат должен быть таким же, с абсолютными, а не относительными путями на выходе.
Преимущества использования идиомы __main__
Одним из ключевых преимуществ является разделение логики, определенной в ваших модулях, от ее выполнения, будь то с помощью if __name__ == '__main__'
«охранного» оператора или __main__.py
файла. Подробности о том, следует ли вам его использовать, как структурировать проект и какие элементы логики и куда должны идти, обычно будут зависеть от того, что делает код и как он предназначен для использования. Вот несколько общих закономерностей, которые следует учитывать.
Тестирование
Представьте, что module_a
из первого примера не имеет инструкции защиты и будет вызывать основную функцию каждый раз, когда модуль куда-то импортируется. Если вы хотите написать тест для function_a
, вам нужно будет импортировать module_a
в свой тестовый сценарий, который немедленно вызовет main()
, а затем function_a()
. В данном конкретном случае это может не иметь большого значения, но если бы function_a
имел более значительный побочный эффект (например, запись файла в указанное место), вы, скорее всего, захотели бы этого избежать или, по крайней мере, иметь больший контроль над ним.
Аргументы командной строки
Другой вариант использования - это модуль или пакет, который предназначен для автономной работы (как сценарий командной строки), но также определяет логику, которая может использоваться (через импорт) в других модулях. Поскольку ваш проект предназначен для запуска из командной строки, он, вероятно, будет иметь некоторую форму обработки аргументов командной строки с использованием argparse
или сопоставимой библиотеки.
Также может потребоваться выполнить другие начальные шаги, такие как чтение и проверка файлов конфигурации, настройка регистратора и т. Д. Эти и другие вещи могут быть ненужными или даже контрпродуктивными, когда модуль импортируется как часть другого проекта.
Импорт
Если в вашем проекте используются библиотеки, которые актуальны только во время его выполнения, имеет смысл импортировать эти библиотеки в __main__.py
и, таким образом, избегать необходимости импортировать их, когда ваш код импортируется в другое место.
Понимание __init__.py
Чуть более распространенную вещь в проектах и модулях Python можно найти - это файл __init__.py
. В официальной документации говорится, что
когда импортируется обычный пакет, этот
__init__.py
файл неявно выполняется, а объекты, которые он определяет, привязываются к именам в пространстве имен пакета.
Это означает, что __init__.py
служит другой цели, чем __main__.py
, и мы можем использовать метод, описанный выше, чтобы понять различия более подробно.
Давайте расширим my_package
сверху и добавим __init__.py
файл:
my_package/ ├── __init__.py ├── __main__.py ├── module_x.py └── module_y.py
где __init__.py
имеет следующее содержание:
Когда мы теперь передаем пакет интерпретатору (python my_package
), мы должны увидеть тот же результат, что и в предыдущем примере, поскольку ничего не изменилось для выполнения пакета таким образом.
Основное различие заключается в том, что мы рассматриваем пакет как реальный пакет и импортируем его.
Импорт пакета
Чтобы разобраться в тонкостях, опишу пошаговый подход. Мы запускаем сеанс интерпретатора Python без каких-либо параметров и используем наш двухстрочный указатель сверху, чтобы получить представление о текущем пространстве имен, а именно:
$ python Python 3.6.9 (default, Oct 8 2020, 12:12:24) [GCC 8.4.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> for k, v in dict(globals()).items(): ... print(f'{repr(k)}: {repr(v)}') ... '__name__': '__main__' '__doc__': None '__package__': None '__loader__': <class '_frozen_importlib.BuiltinImporter'> '__spec__': None '__annotations__': {} '__builtins__': <module 'builtins' (built-in)>
Это очень похоже на случай, когда мы передали модуль напрямую интерпретатору. Как и следовало ожидать, интерпретатор создал модуль с именем __main__
и заполнил некоторые из связанных с модулем переменных.
На следующем этапе мы импортируем my_package
:
>>> import my_package globals init: '__name__': 'my_package' '__doc__': 'Init' '__package__': 'my_package' '__loader__': <_frozen_importlib_external.SourceFileLoader ...> '__spec__': ModuleSpec(name='my_package', loader=<_frozen_importlib_external.SourceFileLoader ...>, origin='/path/to/my_package/__init__.py', submodule_search_locations=['/path/to/my_package']) '__path__': ['/path/to/my_package'] '__file__': '/path/to/my_package/__init__.py' '__cached__': '/path/to/my_package/__pycache__/__init__.cpython-36.pyc' '__builtins__': {'__name__': 'builtins', ...}
и просмотрите распечатанные переменные пространства имен для __init__.py
.
Обратите внимание, как в этом случае __name__
, а также __package__
устанавливается равным строковому значению my_package
. Это показывает нам, что все в __init__.py
было выполнено при импорте (включая функцию определение), и что был создан новый модуль (а также пространство имен), который содержит привязки ко всему, что определено в __init__.py
.
После импорта my_package
в сеанс интерпретатора давайте воспользуемся globals()
, чтобы проверить пространство имен и убедиться, что пакет доступен:
>>> for k, v in dict(globals()).items(): ... print(f'{repr(k)}: {repr(v)}') '__name__': '__main__' '__doc__': None ... ... 'my_package': <module 'my_package' from '/path/to/my_package/__init__.py'>
В последней строке теперь должно отображаться my_package
.
Чтобы еще больше убедить себя, мы можем выполнить несколько таких проверок:
>>> my_package.__name__ 'my_package' >>> my_package.__file__ '/path/to/my_package/__init__.py'
И в итоге:
>>> my_package.package_level_function() package level function
Функция, определенная в __init__.py
, доступна немедленно, однако два модуля (module_x
и module_y
) недоступны:
>>> my_package.module_x Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: module 'my_package' has no attribute 'module_x'
Импорт модуля из пакета
Чтобы сделать модули доступными, а также для дальнейшего изучения того, что происходит, когда мы импортируем один из модулей из my_package
, давайте запустим новый сеанс интерпретатора и выполним следующий оператор импорта:
$ python ... >>> from my_package import module_x
В этом случае мы видим две вещи:
Распечатываются переменные пространства имен в __init__.py
, за которыми следуют переменные пространства имен в module_x
:
globals init: '__name__': 'my_package' '__doc__': 'Init module' '__package__': 'my_package' ... ... '__builtins__': {'__name__': 'builtins', ...} globals module X: '__name__': 'my_package.module_x' '__doc__': 'Module X' '__package__': 'my_package' '__loader__': <_frozen_importlib_external.SourceFileLoader ...> '__spec__': ModuleSpec(name='my_package.module_x', loader=<_frozen_importlib_external.SourceFileLoader ...>, origin='/path/to/my_package/module_x.py') '__file__': '/path/to/my_package/module_x.py' '__cached__': '/path/to/my_package/__pycache__/module_x.cpython-36.pyc' '__builtins__': {'__name__': 'builtins', ...}
Другими словами, все в __init__.py
было выполнено до импорта module_x
и выполнения всего внутри него.
Также обратите внимание на то, что module_x
__name__
было установлено на полное имя модуля, а __package__
было установлено на my_package
. Распечатывая переменные, главное пространство имен показывает нам:
>>> for k, v in dict(globals()).items(): ... print(f'{repr(k)}: {repr(v)}') ... '__name__': '__main__' '__doc__': None '__package__': None '__loader__': <class '_frozen_importlib.BuiltinImporter'> '__spec__': None '__annotations__': {} '__builtins__': <module 'builtins' (built-in)> 'module_x': <module 'my_package.module_x' from '/path/to/my_package/module_x.py'>
Другими словами, последняя строка сообщает нам, что my_package.module_x
теперь привязан к переменной с именем module_x
в пространстве имен и что она доступна (а my_package
- нет):
>>> module_x.function_x() function x >>> >>> my_package.package_level_function() Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'my_package' is not defined
Это неудивительно, поскольку my_package
не отображается в переменных пространства имен.
Чтобы подробнее узнать, что происходит, давайте взглянем на sys.modules
. В том же сеансе выполните следующие несколько строк (вывод усечен для удобства чтения):
>>> import sys >>> for k, v in sorted(sys.modules.items()): ... print(f'{repr(k)}: {repr(v)}') ... '__future__': <module '__future__' from '/usr/lib/python3.6/__future__.py'> '__main__': <module '__main__' (built-in)> ... 'my_package': <module 'my_package' from '/path/to/my_package/__init__.py'> 'my_package.module_x': <module 'my_package.module_x' from '/path/to/my_package/module_x.py'> ... 'zlib': <module 'zlib' (built-in)>
Распечатываемый длинный список снова содержит все модули, которые были загружены интерпретатором до этого момента. Мы замечаем, что my_package
и my_package.module_x
были загружены, но только my_package.module_x
был импортирован и привязан к имени в основном пространстве имен (что означает, что оно отображается при печати globals
и доступен в интерпретаторе).
Импорт модуля пакета
Давайте посмотрим, что произойдет, когда мы импортируем модуль, используя его полное имя. Для этого мы запускаем новый сеанс интерпретатора Python и вводим следующее:
$ python ... >>> import my_package.module_x
Вывод очень похож на предыдущий случай, печатаются переменные в пространстве имен __init__.py
, за которыми следуют переменные в module_x
:
globals init: '__name__': 'my_package' '__doc__': 'Init module' '__package__': 'my_package' ... globals module X: '__name__': 'my_package.module_x' '__doc__': 'Module X' '__package__': 'my_package' ...
Разница становится более очевидной, когда мы проверяем переменные в основном пространстве имен:
>>> for k, v in dict(globals()).items(): ... print(f'{repr(k)}: {repr(v)}') ... '__name__': '__main__' '__doc__': None '__package__': None '__loader__': <class '_frozen_importlib.BuiltinImporter'> '__spec__': None '__annotations__': {} '__builtins__': <module 'builtins' (built-in)> 'my_package': <module 'my_package' from '/path/to/my_package/__init__.py'>
Мы видим, что в отличие от метода выше, my_package
(вместо module_x
) теперь определен в пространстве имен, и мы не можем получить доступ к module_x
напрямую, но должны использовать его полное имя:
>>> module_x.function_x() Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'module_x' is not defined >>> >>> my_package.module_x.function_x() function x
Также можно вызывать функции и все остальное, определенное в __init__.py
:
>>> my_package.package_level_function() package level function
Чтобы лучше понять, что происходит, давайте проверим my_package
с помощью функции Python dir
(которая создает список, аналогичный globals.keys()
):
>>> for v in dir(my_package): ... print(repr(v)) ... '__builtins__' '__cached__' '__doc__' '__file__' '__loader__' '__name__' '__package__' '__path__' '__spec__' 'module_x' 'package_level_function'
Как мы видим, module_x
стал частью пространства имен my_package
, что объясняет, почему мы не можем вызывать его напрямую.
Другими словами, таким образом мы загрузили, импортировали и привязали my_package
к переменной в основных пространствах имен, одновременно привязывая module_x
к переменной в пространстве имен my_package
. Это поведение также описано в документации функции __import__
, которая вызывается во время импорта.
Импорт * из пакета
Официальное руководство по Python прекрасно объясняет, что происходит, когда вы импортируете *
из пакета. Давайте воспользуемся методом, описанным в этой статье, чтобы лучше понять это на новом сеансе интерпретатора:
$ python ... >>> from my_package import * globals init: '__name__': 'my_package' '__doc__': 'Init module' '__package__': 'my_package' '__loader__': <_frozen_importlib_external.SourceFileLoader ...> '__spec__': ModuleSpec(name='my_package', loader=<_frozen_importlib_external.SourceFileLoader ...>, origin='/path/to/my_package/ __init__.py', submodule_search_locations=['/path/to/my_package']) '__path__': ['/path/to/my_package'] '__file__': '/path/to/my_package/__init__.py' '__cached__': '/path/to/my_package/__pycache__/__init__.cpython-36.pyc' '__builtins__': {'__name__': 'builtins', ...}
Как и ожидалось, код в __init__.py
был выполнен, но ничего больше. Мы можем дополнительно проверить это, проверив переменные пространства имен:
>>> for k, v in dict(globals()).items(): ... print(f'{repr(k)}: {repr(v)}') ... '__name__': '__main__' '__doc__': None '__package__': None '__loader__': <class '_frozen_importlib.BuiltinImporter'> '__spec__': None '__annotations__': {} '__builtins__': <module 'builtins' (built-in)> 'package_level_function': <function package_level_function ...>
Единственный (дополнительный) доступный объект - это package_level_function
, в то время как module_x
и module_y
не были импортированы и загружены.
Если мы хотим изменить это, мы можем следовать инструкциям в руководстве и добавить __all__
в пакет. __init__.py
- идеальное место для добавления этой переменной, поэтому мы модифицируем файл, чтобы он выглядел так:
В новом сеансе интерпретатора мы повторяем импорт:
$ python ... >>> from my_package import *
Как и ожидалось, мы видим __init__.py
распечаток, за которыми следуют module_x
распечатки:
globals init: '__name__': 'my_package' '__doc__': 'Init module' '__package__': 'my_package' ... '__all__': ['module_x'] globals module X: '__name__': 'my_package.module_x' '__doc__': 'Module X' '__package__': 'my_package' ...
Обратите внимание, что __all__
теперь определен в пространствах имен __init__
, поэтому module_x
импортируется.
Теперь мы можем дополнительно изучить основное пространство имен:
>>> for k, v in dict(globals()).items(): ... print(f'{repr(k)}: {repr(v)}') ... '__name__': '__main__' '__doc__': None '__package__': None '__loader__': <class '_frozen_importlib.BuiltinImporter'> '__spec__': None '__annotations__': {} '__builtins__': <module 'builtins' (built-in)> 'module_x': <module 'my_package.module_x' from '/path/to/my_package/module_x.py'>
Как и ожидалось, мы видим module_x
в пространстве имен, однако package_level_function
недоступен в этом случае напрямую (как и сам my_package
). Другими словами, исключение вещей из __all__
позволяет вам «скрывать» объекты, функции, переменные и т. Д., Определенные в __init__.py
, которые вы можете использовать для инициализации вашего пакета, но которые не должны быть открыты пользователю (например, потому что они содержат имя, которое может конфликтовать с другим импортом).
Вывод
Есть много тонкостей, связанных с механизмом импорта Python, но это мощный инструмент, который делает за вас много тяжелой работы, когда дело доходит до поиска модулей в вашем дереве файлов, их загрузки и импорта. Это также дает вам большую гибкость и удобство при импорте модулей.
Python также позволяет разделить логику в вашем проекте таким образом, чтобы помочь другим легче понять структуру проекта.
Механизм импорта - это гораздо больше, и в целом официальное руководство по Python является отличным справочником для программистов среднего и продвинутого уровней.
Если вы похожи на меня, то простое наблюдение за внутренним устройством, подобное тому, которое описано в этой статье, - отличный способ закрепить ваши знания и лучше понять детали языка.