Python循环导入

Python Circular Imports

什么是循环依赖?

当两个或更多模块相互依赖时,就会发生循环依赖。 这是因为每个模块都是根据另一个模块定义的(见图1)。

例如:

1
2
functionA():
    functionB()

1
2
functionB():
    functionA()

上面的代码描述了一个相当明显的循环依赖。 functionA()调用functionB(),因此取决于它,functionB()调用functionA()。 这种循环依赖有一些明显的问题,我们将在下一节中进一步描述。

图1

循环依赖问题

循环依赖关系会在代码中引起很多问题。 例如,它可能会在模块之间产生紧密的耦合,从而降低代码的可重用性。 从长远来看,这一事实也使代码难以维护。

此外,循环依赖关系可能是潜在故障的根源,例如无限递归,内存泄漏和级联效应。 如果您不小心,并且代码中有循环依赖关系,则调试它可能导致的许多潜在问题可能非常困难。

什么是循环进口?

循环导入是循环依赖的一种形式,它是使用Python中的import语句创建的。

例如,让我们分析以下代码:

1
2
3
4
5
6
7
8
# module1
import module2

def function1():
    module2.function2()

def function3():
    print('Goodbye, World!')

1
2
3
4
5
6
# module2
import module1

def function2():
    print('Hello, World!')
    module1.function3()

1
2
3
4
5
# __init__.py

import module1

module1.function1()

当Python导入模块时,它将检查模块注册表以查看模块是否已导入。 如果模块已经注册,Python将使用缓存中的现有对象。 模块注册表是已按模块名称初始化和索引的模块表。 可以通过sys.modules访问该表。

如果尚未注册,Python会找到该模块,并在必要时对其进行初始化,然后在新模块的名称空间中执行该模块。

在我们的示例中,当Python到达import module2时,它将加载并执行它。 但是,module2还调用module1,该模块又定义了function1()

function2()尝试调用module1的function3()时,会发生此问题。 由于首先加载了module1,然后又加载了module2,直到它到达function3()为止,该函数尚未定义,并在调用时引发错误:

1
2
3
4
5
6
7
8
9
10
$ python __init__.py
Hello, World!
Traceback (most recent call last):
  File"__init__.py", line 3, in <module>
    module1.function1()
  File"/Users/scott/projects/sandbox/python/circular-dep-test/module1/__init__.py", line 5, in function1
    module2.function2()
  File"/Users/scott/projects/sandbox/python/circular-dep-test/module2/__init__.py", line 6, in function2
    module1.function3()
AttributeError: 'module' object has no attribute 'function3'

如何解决循环依赖

通常,圆形进口是不良设计的结果。 对程序进行更深入的分析可能会得出结论,实际上并不需要依赖项,或者可以将依赖的功能移到不包含循环引用的其他模块中。

一个简单的解决方案是有时两个模块都可以合并为一个更大的模块。 上面示例中的结果代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
# module 1 & 2

def function1():
    function2()

def function2():
    print('Hello, World!')
    function3()

def function3():
    print('Goodbye, World!')

function1()

但是,合并的模块可能具有一些不相关的功能(紧密耦合),如果两个模块中已经有很多代码,合并后的模块可能会变得非常大。

因此,如果这不起作用,则另一种解决方案可能是推迟导入module2以便仅在需要时才导入它。 这可以通过将模块2的导入放在function1()的定义中来完成:

1
2
3
4
5
6
7
8
# module 1

def function1():
    import module2
    module2.function2()

def function3():
    print('Goodbye, World!')

在这种情况下,Python将能够在module1中加载所有功能,然后仅在需要时才加载module2。

这种方法与Python语法并不矛盾,正如Python文档所述:"这是惯例,但并非必须将所有import语句放在模块(或脚本)的开头"。

Python文档还指出,建议使用import X而不是其他语句,例如from module import *from module import a,b,c

即使没有循环依赖关系,您也可能会看到许多使用延迟导入的代码库,这会加快启动时间,因此,这根本不被认为是错误的做法(尽管根据您的项目,这可能是错误的设计) 。

包起来

循环导入是循环引用的一种特殊情况。 通常,可以通过更好的代码设计来解决它们。 但是,有时,最终的设计可能包含大量代码,或者混合了不相关的功能(紧密耦合)。

您是否以自己的代码运行过循环导入? 如果是这样,您如何解决? 让我们在评论中知道!