以下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
>>>
(在conda-forge python 3.6.7上测试)
更一般而言,是否有任何方法可以跟踪或调试python导入过程,以了解cpython在哪里以及将不会搜索模块(以及为什么)?尤其是在处理相对导入,子包,包中的脚本以及不同的调用方式(例如当前工作目录是在包的内部还是外部)时?
答案 0 :(得分:0)
此行为是cpython中的错误。
每个python import
语句都转换为对__import__
内置python函数的一个或多个调用。 (这是documented,可以被拦截。)
在cpython中,有__import__
的两种实现:有python reference实现(在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__
全局)。如果此属性没有名为foo
的属性,则IMPORT_FROM
会引发一个ImportError
。
在交互式解释程序外壳程序或简单脚本中,__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
(而不是预期的{ {1}})。