python - 如何跟踪python相对进口?
问题描述
一般来说,有没有办法跟踪或调试 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 中的一个错误。
每个 pythonimport
语句都被转换为对__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
(即为X
空白且级别为 1)然后IMPORT_NAME
尝试返回当前父包的模块对象(例如,由__package__
全局命名的任何内容)。如果 this 没有命名属性foo
,则IMPORT_FROM
引发ImportError
.
在交互式解释器 shell 或简单脚本中,__package__
是None
. 在这种情况下:
importlib.__import__
会引发ImportError
(尝试在没有已知父包的情况下进行相对导入),但是builtins.__import__
返回模块__main__
(内置),即python顶层脚本环境。
这是关键的区别。由于所有全局变量都是__main__
模块的属性,因此这种不当行为会导致:
>>> foo = 'oops'
>>> from . import foo as fubar
>>> fubar
'oops'
还有另一种不当行为:如果尝试更深层次的相对导入(超出顶级包 eg from ..... import foo
)然后builtins.__import__
引发 a ValueError
(而不是预期的ImportError
)。
更新:此处探讨的两个错误随后在 cpython 中得到修复(请参阅bpo-37409)。除了上述关于 python 语法如何与 python 字节码指令相关的见解之外,设置builtins.__import__ = importlib.__import__
(使用本机参考实现)应该有助于使用普通 python 调试器逐步完成任何导入过程。
推荐阅读
- python - 通过比较另一字典中的键从一个字典返回值
- python - 反向查找字典
- octobercms - 在 OctoberCMS 的 PHP 部分中访问部分组件
- python - Python中的递归堆栈
- linkedin - 链接中的元标记类型问题
- sql - 为什么我得到错误缺少右括号
- google-cloud-platform - Google Cloud TPU:gcloud compute tpus create failed with permission denied
- javascript - 如何通过数组内的“sortby”键对该对象进行排序?
- yaml - YAML 将多个锚点合并到同一个映射中
- r - 根据 R 中两个变量的值过滤数据框