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__'):
The __path__
访问就在那里,在线 1019。因为你__getattr__
回报None
对于所有输入,hasattr
回报True
在这里,你的模块看起来像一个包,代码继续。 (如果hasattr
已经回来了False
, _handle_fromlist
此时将中止。)
这里的“fromlist”将包含您请求的名称,["test"]
,所以我们进入 for 循环x="test"
第 1032 行有“额外”调用:
elif not hasattr(module, x):
from lib import test
只会尝试加载lib.test
子模块如果lib
还没有test
属性。这个检查是测试该属性是否存在,看看是否_handle_fromlist
需要尝试加载子模块。
如果您第一次和第二次调用返回不同的值__getattr__
名称为“test”,那么返回的第二个值就是实际收到的值main.py
.