TL; DR первый напечатанный тест является побочным эффектом реализации from import, т.е. он печатается во время создания модуля lib
. Второй тест - это последующий доступ к динамическому атрибуту непосредственно в модуле.
Зная, что importlib
реализован в коде Python, немного измените свой lib.py
, чтобы также выгружать след:
# lib.py
from traceback import print_stack
def __getattr__(name):
print_stack()
print(name)
print("-" * 80)
Это дает подсказку для точного определения местоположения библиотеки в importlib, которая запускает двойной доступ к атрибутам:
$ python3 main.py
File "main.py", line 3, in <module>
from lib import test
File "<frozen importlib._bootstrap>", line 1019, in _handle_fromlist
File "/private/tmp/lib.py", line 5, in __getattr__
print_stack()
__path__
--------------------------------------------------------------------------------
File "main.py", line 3, in <module>
from lib import test
File "<frozen importlib._bootstrap>", line 1032, in _handle_fromlist
File "/private/tmp/lib.py", line 5, in __getattr__
print_stack()
test
--------------------------------------------------------------------------------
File "main.py", line 3, in <module>
from lib import test
File "/private/tmp/lib.py", line 5, in __getattr__
print_stack()
test
--------------------------------------------------------------------------------
Теперь мы можем легко найти ответ с помощью RTFS (ниже я использую Python v3.7.6, переключите git на точный тег, который вы используете в случае другой версии). Посмотрите importlib._bootstrap. _handle_fromlist
на указанные номера строк.
_handle_fromlist
- помощник, предназначенный для загрузки подмодулей пакета при импорте from
. Шаг 1 - проверить, является ли модуль вообще пакетом:
if hasattr(module, '__path__'):
Доступ __path__
поступает туда, в строке 1019. Поскольку ваш __getattr__
возвращает None
для всех входов, hasattr
возвращает True
здесь, поэтому ваш модуль выглядит как пакет, и код продолжается. (Если hasattr
вернул False
, _handle_fromlist
на этом этапе прервется.)
Список из списка будет иметь запрошенное вами имя ["test"]
, поэтому мы переходим в цикл for с x="test"
, а в строке 1032 есть дополнительный вызов:
elif not hasattr(module, x):
from lib import test
будет пытаться загрузить подмодуль lib.test
, только если lib
еще не имеет атрибута test
. Эта проверка проверяет, существует ли атрибут, чтобы увидеть, нужно ли _handle_fromlist
попытаться загрузить подмодуль.
Если вы вернете разные значения для первого и второго вызова __getattr__
с проверкой имени, то второе возвращаемое значение будет тем, которое будет фактически получено в main.py
.
person
wim
schedule
22.03.2020
__path__
является частью механизма импорта. См. stackoverflow.com/q/2699287/1126841. - person chepner   schedule 20.03.2020test
отправляется 2 раза? - person Julien   schedule 21.03.2020