[英]How to trace python relative imports?
一般来说,有没有办法跟踪或调试 python 导入过程,例如了解 cpython 在哪里搜索模块(以及为什么)? 尤其是在处理相对导入、子包、包内脚本以及调用它们的不同方式时(例如当前工作目录是包内还是包外)?
例如,以下行为(在 conda-forge python 3.6.7 上测试)在我看来像是一个错误。 (更新:此特定示例随后在 Python 的后续版本中得到修复。尽管如此,调试技术可能仍然具有更广泛的相关性,并提供对语言如何运行的深入了解。)
>>> from curses import textpad
>>> from . import textpad # <-- expected to fail?
>>> from . import ascii
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: cannot import name 'ascii'
>>> from curses import ascii
>>> from . import textpad
>>> from . import ascii
>>>
这种行为是 cpython 中的一个错误。
每个 python import
语句都被转换为对__import__
内置 python 函数的一次或多次调用。 (这是记录在案的,可以拦截。)
在 cpython 中有两种__import__
实现:一个是 python 参考实现(在importlib
标准库中),一个是默认调用的 C 实现(可以通过builtins
标准库访问或拦截)。
这是一个探讨该问题的脚本(注意curses.ascii
和curses.textpad
是python 标准库中的一些模块):
commands = ['from curses import ascii',
'from . import ascii',
'from . import textpad']
def mock(name, globals=None, locals=None, fromlist=(), level=0):
print(' __import__ :', repr(name), ':', fromlist, ':', level)
return alternate(name, globals, locals, fromlist, level)
import builtins
import importlib._bootstrap
original = builtins.__import__
builtins.__import__ = mock
for implementation in ['original', 'importlib._bootstrap.__import__']:
print(implementation.upper(), '\n')
alternate = eval(implementation)
try:
for command in commands:
print(command)
exec(command)
except ImportError as err:
print(' ', repr(err), '\n\n')
输出表明,与参考实现不同,内置的 cpython 无法在尝试相对导入之前检查父包:
ORIGINAL
from curses import ascii
__import__ : 'curses' : ('ascii',) : 0
__import__ : '_curses' : ('*',) : 0
__import__ : 'os' : None : 0
__import__ : 'sys' : None : 0
from . import ascii
__import__ : '' : ('ascii',) : 1
from . import textpad
__import__ : '' : ('textpad',) : 1
ImportError("cannot import name 'textpad'",)
IMPORTLIB._BOOTSTRAP.__IMPORT__
from curses import ascii
__import__ : 'curses' : ('ascii',) : 0
from . import ascii
__import__ : '' : ('ascii',) : 1
ImportError('attempted relative import with no known parent package',)
在 cpython 中, from [...][X] import Y [as Z]
语句被翻译成两个主要的字节码指令(加上一些内务指令,以在堆栈和常量/变量列表之间适当地加载和保存):
IMPORT_NAME
:这会调用builtins.__import__
。 调用参数是指令参数(要返回的模块的名称X
)、解释器框架的一些当前状态( globals()
和locals()
),以及从堆栈中取出的两个项目(列表Y
可能包含子模块导入,以及相对级别,即[...]
的数量)。 该调用应返回一个模块对象,该对象放置在堆栈上。IMPORT_FROM
:这会检查堆栈顶部的模块,并从其属性Y
获取一个对象(它也留在堆栈上)。 (这些与dis
库一起记录并在ceval.c
实现。)
如果我们尝试from . import foo
from . import foo
(即X
为空且级别为 1)然后IMPORT_NAME
尝试返回当前父包的模块对象(例如, __package__
全局命名的任何内容)。 如果它没有名为foo
属性,则IMPORT_FROM
会引发ImportError
。
在交互式解释器 shell 或简单脚本中, __package__
是None
。 在这种情况下:
importlib.__import__
会引发一个ImportError
(尝试相对导入而没有已知的父包),但是builtins.__import__
返回模块__main__
(内置),这是 python 顶级脚本环境。 这是关键的区别。 由于所有全局变量都是__main__
模块的属性,因此这种不当行为会导致:
>>> foo = 'oops'
>>> from . import foo as fubar
>>> fubar
'oops'
还有另一个错误行为:如果尝试更深层次的相对导入(超出顶级包,例如from ..... import foo
),那么builtins.__import__
会引发ValueError
(而不是预期的ImportError
)。
更新:此处探讨的两个错误随后都在 cpython 中修复(请参阅bpo-37409 )。 除了上面对 python 语法与 python 字节码指令的关系的洞察之外,设置builtins.__import__ = importlib.__import__
(使用本机参考实现)应该有助于使用普通 python 调试器逐步完成任何导入过程。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.