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

时间:2018-08-01 17:57:41

标签: python python-3.x python-2.7 python-import circular-dependency

注意:这是关于导入模块,而不是从这些模块中导入类或函数,因此,我认为这与“ ImportError:无法导入”名称”的结果,因此,至少我没有找到与此匹配的内容。

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

为了重现此问题,请创建三个对其具有循环依赖性的模块。

首先创建一个包:

$ 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引用aA) cC)。

而且,实际上,使用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 2上? (参考PEP,代码,发行说明或有关Python 3中此问题的修补程序的文章。)

  • 有什么方法可以避免Python 2中的此问题,同时又不会破坏模块的循环依赖关系?我相信并非所有循环依赖项都会导致此问题(即使在Python 2中也是如此),所以我想知道哪些情况是安全的,哪些情况不是...

2 个答案:

答案 0 :(得分:4)

Python开始加载pkg.a模块时,它会将sys.modules['pkg.a']设置为相应的模块对象,但仅将a模块对象的pkg属性设置为加载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 aa模块对象上查找pkg属性,但它不存在,因为该属性仅在pkg.a完成加载时设置。

在Python 2上就是这样。导入结束。 ImportError在这里。在Python 3(尤其是3.5+)上,由于他们想鼓励相对导入,而这种行为确实很不方便,因此from导入又尝试了一步。如果属性查找失败,请 now 尝试sys.modulespkg.asys.modules中,因此导入成功。您可以在issue 17636的CPython问题跟踪器中查看有关此更改的讨论。

答案 1 :(得分:0)

我不确定Python 3如何解决该问题,但我的经验告诉我Python 2确实无法使其正常工作。解决此问题的正确方法是:

  1. 请注意不要在您的代码中引入
  2. 在需要的地方直接导入函数

我个人更喜欢后者。

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