Python __getattr__ выполняется несколько раз - PullRequest
3 голосов
/ 20 марта 2020

Я пытался реализовать функцию __getattr__, как в следующем примере:

PEP 562 - Модуль __getattr__ и __dir__

И я не понимаю, почему этот простой кусок кода:

# lib.py

def __getattr__(name):
    print(name)

# main.py

from lib import test

выводит:

__path__
test
test

Что такое __path__? Почему оно отправлено на __getattr__? Почему test отправлено 2 раза?

1 Ответ

3 голосов
/ 22 марта 2020

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.

...