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

Understanding behavior of Python imports and circular dependencies

注意:这是关于导入模块而不是那些模块中的类,函数,所以我认为这不是鬃毛"ImportError:无法导入名称"结果的重复,至少我没有找到一个匹配这个。

我确实理解,按名称从模块导入类或函数可能会导致问题,因为如果存在循环依赖关系,模块本身可能尚未完全初始化,但这不是这种情况。

要重现此问题,请创建三个对其具有循环依赖关系的模块。

首先创建一个包:

1
2
$ mkdir pkg
$ touch pkg/__init__.py

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

1
2
3
4
5
6
7
8
9
10
11
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,内容如下:

1
2
3
4
5
6
7
8
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,内容如下:

1
2
3
4
5
6
7
8
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(在顶层目录中):

1
2
3
4
5
6
7
from __future__ import print_function
from __future__ import absolute_import

from pkg import a

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

我期望循环依赖性没有问题,因为在导入期间没有对每个模块中的项的引用(即,除了c.C体内的调用之外,没有从模块b或c引用a.A)。

事实上,使用python3运行它可以正常工作:

1
2
3
4
5
$ 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),它并没有真正起作用,它抱怨循环依赖...

1
2
3
4
5
6
7
8
9
10
11
$ 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 2上? (参考PEP,代码,发行说明或有关Python 3中的修复程序的文章将不胜感激。)

  • 有没有办法在Python 2中避免这个问题,同时仍然没有打破模块的循环依赖?我相信并非所有的循环依赖都会导致这个问题(即使在Python 2中),所以我想知道哪些情况是安全的,哪些情况不是......


当Python开始加载pkg.a模块时,它将sys.modules['pkg.a']设置为相应的模块对象,但它仅在加载pkg.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']来做到这一点。

你知道怎么做from os import open这样的东西,即使os.open是一个函数,而不是一个模块?这种导入不能通过sys.modules['os.open'],因为os.open不是模块而不是sys.modules。相反,所有from导入(包括所有相对导入)都会尝试对他们从中导入名称的模块进行属性查找。 from . import apkg模块对象上查找a属性,但它不存在,因为该属性仅在pkg.a完成加载时设置。

在Python 2上,就是这样。进口结束。 ImportError这里。在Python 3(特别是3.5+)上,因为他们想鼓励相对导入并且这种行为非常不方便,from导入再试一步。如果属性查找失败,现在他们尝试sys.modulespkg.asys.modules中,因此导入成功。您可以在问题17636的CPython问题跟踪器中查看有关此更改的讨论。


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

  • 注意不要在代码中引入它
  • 在您需要的地方导入内部功能
  • 我个人更喜欢后者。

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