TL; DR, первый напечатанный «тест» является побочным эффектом реализации «из импорта», то есть он напечатан при создании модуля lib
. Второй «тест» связан с последующим доступом к атрибуту динамического c непосредственно в модуле.
Зная, что 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
на указанные номера строк.
Во-первых, туда приходит доступ __path__
, строка 1019:
if hasattr(module, '__path__'):
Здесь "fromlist" будет иметь имя, которое вы просили, ["test"]
, поэтому мы go в for-l oop с x="test"
и в строке 1032 есть «дополнительный» вызов:
elif not hasattr(module, x):
Если вы возвращаете разные значения для первого и второго вызова __getattr__
с именем «test», затем возвращается второе значение, которое будет фактически получено в пределах main.py
. Первый вызов, кажется, является только случайным триггером __getattr__
, вызванным реализацией importlib.