Пишите лучше структурированные модули и пакеты

Если вы какое-то время работали с 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 является отличным справочником для программистов среднего и продвинутого уровней.

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