繁体   English   中英

如何跟踪python相对导入?

[英]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.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 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM