文章目录

Python __del__方法在循环引用时的调用时机问题

发布于 2026-04-27 03:13:31 · 浏览 7 次 · 评论 0 条

Python __del__方法在循环引用时的调用时机问题

在 Python 开发中,许多开发者习惯使用 __del__ 方法(析构函数)来释放资源或记录对象销毁日志。然而,当对象之间存在“循环引用”时,__del__ 的调用时机往往与预期不符,导致资源无法及时释放。这不仅会引发内存泄漏,还会让调试变得异常困难。本文将带你复现这个问题,分析其背后的机制,并提供具体的解决方案。


1. 复现正常销毁场景

首先,我们需要明确在非循环引用情况下,__del__ 方法是如何工作的。这能为我们后续的对比提供基准。

编写 一个名为 MyObject 的类,并在其中定义 __del__ 方法用于打印销毁日志:

class MyObject:
    def __init__(self, name):
        self.name = name

    def __del__(self):
        print(f"对象 {self.name} 被销毁了")

创建 两个独立的对象实例,并随即删除 对它们的引用:

# 创建对象
obj_a = MyObject("A")
obj_b = MyObject("B")

# 删除引用
del obj_a
del obj_b

执行 上述代码后,你会立即在控制台看到两行输出,证明 __del__ 方法已被调用:

对象 A 被销毁了
对象 B 被销毁了

这是因为 Python 主要使用“引用计数”机制来管理内存。当 del obj_a 执行 时,obj_a 指向的对象的引用计数降为 0,Python 立即回收内存并触发 __del__


2. 制造循环引用问题

接下来,我们将改变代码结构,让两个对象相互持有对方的引用,从而形成循环引用。

修改 MyObject 类,增加一个 link 属性用于存储其他对象:

class MyObject:
    def __init__(self, name):
        self.name = name
        self.link = None  # 用于指向其他对象

    def __del__(self):
        print(f"对象 {self.name} 被销毁了")

创建 两个对象,并建立 相互引用:

# 创建对象
obj_a = MyObject("A")
obj_b = MyObject("B")

# 建立循环引用:A 指向 B,B 指向 A
obj_a.link = obj_b
obj_b.link = obj_a

# 删除外部引用
del obj_a
del obj_b

观察 控制台输出。你会发现,此时没有任何输出产生。尽管 obj_aobj_b 变量已被删除,但对象的 __del__ 方法并没有被调用。

原因在于:

  1. del obj_a 执行 时,对象 A 的引用计数从 1 降为 1(因为对象 B 的 link 属性仍然引用着 A),并未归零。
  2. 同理,对象 B 的引用计数也停留在 1。
  3. 引用计数机制无法处理这种“环形”依赖,这两个对象因此变成了“不可达”但未被销毁的垃圾数据。

3. 强制垃圾回收

虽然引用计数失效,但 Python 内部还有一个备用机制:“循环垃圾回收器”。它是一个专门检测循环引用的组件,但它通常不会在每次对象删除时立即运行,而是在内存分配达到一定阈值后定期运行。

为了演示,我们可以手动调用 垃圾回收器。

导入 gc 模块,并在删除变量后运行 gc.collect()

import gc

# 定义类... (同上)

# 创建并建立循环引用
obj_a = MyObject("A")
obj_b = MyObject("B")
obj_a.link = obj_b
obj_b.link = obj_a

# 删除外部引用
del obj_a
del obj_b

# 强制进行垃圾回收
print("--- 开始强制回收 ---")
collected = gc.collect()
print(f"--- 回收结束,共回收 {collected} 个对象 ---")

查看 输出结果:

--- 开始强制回收 ---
对象 A 被销毁了
对象 B 被销毁了
--- 回收结束,共回收 2 个对象 ---

此时 __del__ 终于被调用了。

结论:在存在循环引用的情况下,__del__ 方法的调用时机是不确定的,它被延迟到了垃圾回收器下一次运行的时候。


4. 理解调用时机的逻辑

为了更直观地理解 Python 销毁对象的决策过程,可以参考以下逻辑流程。

graph TD A["删除变量引用"] --> B{引用计数 == 0?"} B -- "是" --> C["立即调用 __del__"] B -- "否" --> D{"对象存在于循环引用中?"} D -- "否" --> E["保留在内存中"] D -- "是" --> F["标记为不可达垃圾"] F --> G["等待 GC 运行"] G --> H["自动回收并调用 __del__"]

从流程图中可以看出,只有走“引用计数归零”这条路径,__del__ 才会立即执行。一旦进入“循环引用”分支,就必须等待 GC 运行。


5. 使用弱引用打破循环

依赖 GC 的延迟回收在某些对资源释放时效性要求极高的场景(如数据库连接、文件句柄)是不可接受的。最佳解决方案是打破循环引用,最常用的工具是 weakref(弱引用)。

导入 weakref 模块。

修改 代码,将其中一个对象的引用改为弱引用。弱引用不会增加对象的引用计数,因此不会阻碍对象被销毁。

import weakref

class MyObject:
    def __init__(self, name):
        self.name = name
        self.link = None

    def __del__(self):
        print(f"对象 {self.name} 被销毁了")

# 创建对象
obj_a = MyObject("A")
obj_b = MyObject("B")

# 使用弱引用:A 持有 B 的强引用,B 持有 A 的弱引用
# 这样 B 不再阻碍 A 的销毁
obj_a.link = obj_b
obj_b.link = weakref.ref(obj_a)

# 删除外部引用
del obj_a
del obj_b

分析 执行过程:

  1. del obj_a 执行 时,对象 A 的引用计数降为 0(因为 obj_b.link 是弱引用,不计数)。
  2. 对象 A 立即被销毁,触发 __del__ 并输出日志。
  3. 对象 A 销毁后,它对 B 的强引用(obj_a.link)也随之消失。
  4. 此时对象 B 的引用计数也降为 0,对象 B 被销毁,输出日志。

查看 结果,无需手动调用 gc.collect(),对象也能立即释放:

对象 A 被销毁了
对象 B 被销毁了

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文