了解 Python 导入和循环依赖的行为

Understanding behavior of Python imports and circular dependencies

注意:这是关于导入 模块 而不是 类,这些模块的函数,所以我不认为它是鬃毛 "ImportError: cannot import name" 结果的副本,至少我还没有找到匹配的结果。

我知道按名称从模块导入 类 或函数可能会导致问题,因为如果存在循环依赖,模块本身可能还没有完全初始化,但这里不是这种情况。

为了重现这个问题,创建三个循环依赖它的模块。

首先创建一个包:

$ mkdir pkg
$ touch pkg/__init__.py

然后创建pkg/a.py,内容为:

from __future__ import print_function
from __future__ import absolute_import

from . import b

def A(x):
    print('I am A, x={}.'.format(x))
    b.B(x + 1)

def Z(x):
    print('I am Z, x={}. I\'m done now!'.format(x))

和pkg/b.py,内容为:

from __future__ import print_function
from __future__ import absolute_import

from . import c

def B(x):
    print('I am B, x={}.'.format(x))
    c.C(x * 2)

和pkg/c.py,内容为:

from __future__ import print_function
from __future__ import absolute_import

from . import a

def C(x):
    print('I am C, x={}.'.format(x))
    a.Z(x ** 2)

还有一个 main.py(在顶级目录中)调用它们:

from __future__ import print_function
from __future__ import absolute_import

from pkg import a

if __name__ == '__main__':
    a.A(5)

我预计循环依赖不会有问题,因为在导入期间没有对每个模块中项目的引用(即没有从模块 b 或 c 引用 a.A,除了c.C).

体内的调用

而且,确实,运行 这个 python3 工作得很好:

$ python3 main.py 
I am A, x=5.
I am B, x=6.
I am C, x=12.
I am Z, x=144. I'm done now!

(这是 Debian Stretch 上的 Python 3.5.3,供记录。)

但是对于 python2 (Python 2.7.13),它并没有真正起作用,它抱怨循环依赖...

$ python main.py 
Traceback (most recent call last):
  File "main.py", line 5, in <module>
    from pkg import a
  File "/tmp/circular/pkg/a.py", line 5, in <module>
    from . import b
  File "/tmp/circular/pkg/b.py", line 5, in <module>
    from . import c
  File "/tmp/circular/pkg/c.py", line 5, in <module>
    from . import a
ImportError: cannot import name a

所以我的问题是:

我不确定 Python 3 是如何解决这个问题的,但我的经验告诉 Python 2 确实无法解决问题。解决问题的正确方法是:

  1. 注意不要在你的代码中引入这个
  2. 在你需要的地方导入内部函数

我个人更喜欢后者

关于为什么,Python 中的模块系统在成功加载模块之前不会标记它。所以在你的 "import a",Python 不会知道它已经加载 "a" 直到所有依赖加载,"b" 和 "c" 在它经历整个 "a.py" 文件。所以在处理 "import c" 时,它会再次尝试 "import a" 而不是发现 "a" 是它可以跳过的东西。

当Python开始加载pkg.a模块时,它设置了sys.modules['pkg.a']到相应的模块对象,但是它只设置了[=13]的a属性=] 模块对象在加载 pkg.a 模块的最后。这将在以后相关。


相对导入是 from 导入,它们的行为相同。在 from . import whatever 计算出 . 引用了 pkg 包之后,它继续执行常规的 from pkg import whatever 逻辑。

c.py命中from . import a时,首先看到pkg.a已经在sys.modules中,说明pkg.a已经加载或者是在加载过程中。 (它正在加载,但此代码路径并不关心。)它跳到其工作的第二部分,检索 pkg.a 并将其分配给本地名称空间中的 a 名称,但它不只是检索 sys.modules['pkg.a'] 来执行此操作。

即使 os.open 是一个函数,而不是一个模块,您知道如何做类似 from os import open 的事情吗?这种导入不能通过 sys.modules['os.open'],因为 os.open 不是模块,也不在 sys.modules 中。相反,所有 from 导入, 包括所有相关导入 ,尝试在它们从中导入名称的模块上进行属性查找。 from . import apkg 模块对象上查找 a 属性,但它不存在,因为该属性仅在 pkg.a 完成加载时设置。

在Python2,就这样。导入结束。 ImportError 这里。在 Python 3(特别是 3.5+)上,因为他们想鼓励相对导入并且这种行为确实很不方便,所以 from 导入再尝试一步。如果属性查找失败, 现在 他们尝试 sys.modulespkg.asys.modules 中,所以导入成功。您可以在 issue 17636.

的 CPython 问题跟踪器中查看有关此更改的讨论