Почему имена подмодулей могут быть доступны в __init__.py даже без явного их импорта?

#python #python-import

Вопрос:

Проблема:

 pkg/
    __init__.py
    sub1.py
    sub2.py


$ cat pkg/__init__.py
from .sub2 import *
print("init", dir())

$ cat pkg/sub1.py
from .sub2 import *
print("sub1", dir())

$ cat pkg/sub2.py
def spam():
    ...

$ python -c "import pkg"
init [... 'spam', 'sub2']

$ python -c "import pkg.sub1"
init [... 'spam', 'sub2']
sub1 [... 'spam']
 

Обратите внимание , как sub2 это в пространстве pkg имен, хотя на самом деле я его не импортирую. Я бы ожидал, что будут импортированы только имена внутри sub2 . Почему это не так? Я вижу, что это как-то связано с импортом пакета по сравнению с импортом модуля, потому что:

 $ python -c "import pkg.__init__"
init [... 'spam', 'sub2']
init [... 'spam']
 

Это также, кажется, сбивает с толку mypy ; Я редактирую __init__.py , чтобы явно получить доступ sub2 :

 $ cat pkg/__init__.py
from .sub2 import *
print(sub2)
 

Затем бег mypy pkg дает:

 pkg/__init__.py:2: error: Name "sub2" is not defined
Found 1 error in 1 file (checked 3 source files)
 

Почему это происходит? Является ли это задокументированной функцией? Я должен отметить, что эта «функция» используется в исходном коде Cpython; проверьте, например, Lib/asyncio/__init__.py .

Ответ №1:

Это немного причуда подмодулей, но это задокументированное поведение:

Когда подмодуль загружается с использованием любого механизма (например importlib , API, операторов import или import-from или встроенных __import__() ), привязка помещается в пространство имен родительского модуля к объекту подмодуля. Например, если в пакете spam есть подмодуль foo , после импорта spam.foo у spam него будет атрибут foo , привязанный к подмодулю.

Учитывая знакомые правила привязки имен Python, это может показаться удивительным, но на самом деле это фундаментальная особенность системы импорта. Инвариантное удержание заключается в том, что если у вас есть sys.modules['spam'] и sys.modules['spam.foo'] (как и после вышеупомянутого импорта), последнее должно отображаться как атрибут foo первого.