首页 > 解决方案 > 如何跟踪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
>>>

标签: pythondebuggingpython-import

解决方案


这种行为是 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]语句被翻译成两个主要的字节码指令(加上一些管理指令,以在堆栈和常量/变量列表之间适当地加载和保存):

  1. IMPORT_NAME: 这会调用builtins.__import__. 调用参数是指令参数(X要返回的模块的名称),解释器框架的一些当前状态(globals()locals()),以及从堆栈中取出的两个项目(Y可能包含要导入的子模块的列表,以及相对级别,即的个数[...])。该调用应返回一个模块对象,该对象被放置在堆栈上。
  2. 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 调试器逐步完成任何导入过程。


推荐阅读