如何跟踪python相对导入?

时间:2019-06-26 01:16:00

标签: python debugging python-import

以下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在哪里以及将不会搜索模块(以及为什么)?尤其是在处理相对导入,子包,包中的脚本以及不同的调用方式(例如当前工作目录是在包的内部还是外部)时?

1 个答案:

答案 0 :(得分:0)

此行为是cpython中的错误。

每个python import语句都转换为对__import__内置python函数的一个或多个调用。 (这是documented,可以被拦截。)

在cpython中,有__import__的两种实现:有python reference实现(在importlib标准库中),还有C实现(可以访问或通过builtins标准库拦截),默认情况下会调用。

以下是探讨该问题的脚本(注意curses.asciicurses.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]语句被转换为两个主要的字节码指令(加上一些内务处理指令,以在堆栈和常量/变量列表之间适当地加载和保存):

  1. IMPORT_NAME:这将调用builtins.__import__。调用参数是指令参数(要返回的模块的名称X),解释器框架的某些当前状态(globals()locals()),以及从堆栈中取出的两项(列表Y可能包含要导入的子模块,以及相对级别,即[...]的数量)。预期该调用将返回放置在堆栈上的模块对象。
  2. 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}})。