Python __getattr__ выполняется несколько раз

Я пытался реализовать функцию __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 раза?


person Julien    schedule 20.03.2020    source источник
comment
__path__ является частью механизма импорта. См. stackoverflow.com/q/2699287/1126841.   -  person chepner    schedule 20.03.2020
comment
Спасибо за ваш ответ! Вы знаете, почему test отправляется 2 раза?   -  person Julien    schedule 21.03.2020


Ответы (1)


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