如何处理 Python 类之间的循环依赖?


在本文中,我们将讨论如何处理 Python 类之间的循环依赖。首先,让我们了解什么是循环依赖。

当两个或多个模块相互依赖时,这被称为循环依赖。这是因为每个模块都是根据另一个模块定义的。

以下是循环依赖的示例

functionE():
   functionF()

以及

functionF():
   functionE()

上面显示的代码清楚地显示了循环依赖。FunctionA() 调用 functionB(),后者依赖于它,而 functionB() 调用 functionA()。这种循环依赖有一些明显的问题,我们将在下一节中更详细地讨论。


循环依赖的问题

循环依赖可能会导致代码出现各种问题。例如,它可能导致模块之间紧密耦合,这将限制代码重用。这方面也使长期代码维护更具挑战性。

循环依赖也可能是内存泄漏、无限递归和级联效应等问题的根源。当您的代码包含循环依赖时,解决它产生的许多可能问题可能非常具有挑战性,如果您不小心,这种情况可能会发生。

示例

当参与循环引用的任何对象的类具有唯一的 __del__ 函数时,就会出现问题。以下是一个显示循环依赖出现问题的示例:

class Python: def __init__(self): print("Object Python is Created") def __del__(self): print("Object Python is Destroyed") class Program: def __init__(self): print("Object Program is Created") def __del__(self): print("Object Program is Destroyed") #create the two objects Py = Python() Pr = Program() #set up the circular reference Py.Pr = Pr Pr.Py = Py #delete the objects del Py del Pr

输出

在这里,Py 和 Pr 对象都有一个自定义的 __del__ 函数,并且相互持有引用。最终,当我们尝试手动删除对象时,__del__ 方法没有被调用,这表明对象没有被销毁,而是导致了内存泄漏。

在这种情况下,Python 的垃圾收集器无法收集对象以进行内存清理,因为它不确定以什么顺序调用 __del__ 函数。

Object Python is Created
Object Program is Created
Object Python is Destroyed
Object Program is Destroyed

修复 Python 中循环依赖中的内存泄漏

循环引用会导致内存泄漏,可以通过两种方式避免,即手动清除每个引用和使用 Python 中的weakref() 函数。

手动删除每个引用不是一个理想的选择,因为weakref() 消除了程序员需要考虑删除引用的时间的需要。

在 Python 中,weakref 函数提供了一个弱引用,它不足以维持对象的生存期。当对对象的唯一剩余引用是弱引用时,该对象可以被垃圾收集器自由销毁,以便其内存可以被另一个对象使用。

示例

以下示例显示了如何修复循环依赖中的内存泄漏:

import weakref class Python: def __init__(self): print("Object Python is Created") def __del__(self): print("Object Python is Destroyed") class Program: def __init__(self): print("Object Program is Created") def __del__(self): print("Object Program is Destroyed") #create the two objects Py = Python() Pr = Program() #set up the weak circular reference Py.Pr = weakref.ref(Pr) Pr.Py = weakref.ref(Py) #delete the objects del Py del Pr

输出

正如您所看到的,这次使用了两种 __del__ 方法,证明对象已成功从内存中删除。

Object Python is Created
Object Program is Created
Object Python is Destroyed
Object Program is Destroyed

通过循环导入的循环依赖

Python 中的 import 语句会创建循环导入,这是一种循环依赖。

示例

以下示例说明了这一点。假设我们创建了 3 个 Python 文件,如下所示:

Example1.py

# module3 import module4 def func8(): module4.func4() def func9(): print('Welcome to TutorialsPoint')

Example2.py

# module4 import module4 def func4(): print('Thank You!') module3.func9()

Example3.py

# __init__.py import module3 module3.func8()

Python 在导入模块时检查模块注册表以查看它是否已被导入。如果模块已被注册,Python 将使用缓存中先前存在的对象。模块注册表是一个已初始化模块的表,使用模块名称作为索引。sys.modules 提供对该表的访问。

如果模块未注册,Python 会找到它,根据需要初始化它,然后在新模块的命名空间中执行它。

在上面的示例中,Python 在到达 import module4 时加载并运行。但是,module3 也需要 module4,因为它定义了 func8()。

输出

当 func4() 尝试调用 module3 中的 func9() 时,问题就出现了。func9() 尚未定义并返回错误,因为 module3 先加载,它在访问它之前加载了 module4:

$ python __init__.py
Thank You!
Traceback (most recent call last):
   File "__init__.py", line 3, in 
   Module3.func8()
File "C:\Users\Lenovo\Desktop\module3\__init__.py", line 5, in func8
   Module4.func4()
File "C:\Users\Lenovo\Desktop\module4\__init__.py", line 6, in func4
   module4.func9()
AttributeError: 'module' object has no attribute 'func9

修复上述循环依赖

循环导入通常是不良设计的结

有时,将两个模块合并到一个更大的模块中是一个简单的选择。

示例

来自上述示例的最终代码将类似于上面给出的解释:

# module 3 & 4 def func8(): func4() def func4(): print('Welcome to TutorialsPoint') func9() def func9(): print('Thank You!') func8()

输出

以下是上述代码的输出:

Welcome to TutorialsPoint
Thank You!

注意 - 但是,如果两个模块已经包含大量代码,合并后的模块可能包含一些不相关的函数(紧密耦合),并且可能会大幅增长。

如果它不起作用,另一种选择可能是延迟导入 module4,只在需要时导入它。为此,请将 module4 的导入包含在 func8() 的定义中,如下所示:

# module 3 def func8(): import module4 module4.func4() def func9(): print('Thank You!')

在上述情况下,Python 将能够加载 module3 中的所有函数,并且仅在需要时加载 module4。

通常的做法是在模块(或脚本)的开头插入所有 import 语句,但这不是必需的,“这种方法不会违反 Python 语法。

更新于:2022年11月23日

4K+ 浏览量

启动你的职业生涯

完成课程获得认证

开始学习
广告