跳到主要内容

对象生命周期、垃圾回收与弱引用

本节目标

查询 Python 对象可达性、CPython 引用计数边界、循环垃圾回收、终结和弱引用。

对象是否仍可从程序的根可达,和外部资源是否已经关闭,是两件不同的事。本文以 对象、值和类型__del__ 终结gc 模块weakref 模块为准。需要在异常或正常路径都关闭文件、锁和连接时,应接续异常、上下文管理器与资源管理中的 withfinally;异步资源的退出协议见async / await、异步迭代与异步上下文

引用、可达性与对象生命周期

名称、容器项、属性和闭包单元格都可以持有对象的引用。对象从执行中的根、全局或其他可达对象都无法再到达时,才是“不可达”;局部作用域结束或某个名称被解绑只会减少一条可能的路径,不能作为资源已经释放的证明。

Python 不保证对象变得不可达后立即回收:实现可以推迟回收,甚至省略回收,只要不回收仍可达的对象。因此,对象的内存生命周期不能替代文件、网络连接或锁的所有权协议。

record = {"handle": connection}
alias = record["handle"]
del record["handle"]
# alias 仍引用同一个 connection

调用方应在拥有资源的控制流中显式 close(),或让 with / finally 负责退出;不要把“离开作用域”当作可移植的清理操作。

引用计数的 CPython 实现边界

CPython 目前以引用计数为主,并辅以循环垃圾的延迟检测;这是一项 CPython 实现细节,不是 Python 语言保证。它常常让无环对象在最后一个强引用消失后很快被处理,但调试器、追踪器、临时引用和其他 Python 实现都可能改变可观察时机。

因此,不要用 sys.getrefcount() 的具体数值定义业务逻辑,也不要从一次 CPython 试验推导出所有实现或所有版本的销毁顺序。语言层面可靠的是可达性约束:仍可达的对象不会被回收;何时处理不可达对象不是资源管理合同。

def cache_item(item):
return item

saved = cache_item(resource)

即使调用方随后删除了 resourcesaved 仍是强引用。应先梳理引用图和所有权,再讨论 CPython 的内存回收行为。

循环引用与分代垃圾回收

两个对象互相持有引用、而外部不再持有它们时,会形成不可达循环。CPython 的 gc 接口用于其循环垃圾收集器;自动触发受阈值和实现策略影响,不能把下一次自动扫描的时间写成程序条件。为让示例的观察点稳定,下列脚本在删除外部绑定后显式调用 gc.collect(),但不检查或输出其返回的计数。

lifecycle_report.py
import gc
import sys
import weakref


class Node:
def __init__(self) -> None:
self.other = None


first = Node()
second = Node()
first.other = second
second.other = first
cycle_ref = weakref.ref(first)
del first, second
gc.collect()

finalizer_events: list[str] = []
target = Node()
finalizer = weakref.finalize(target, finalizer_events.append, "finalized")
del target
gc.collect()

values: weakref.WeakValueDictionary[str, Node] = weakref.WeakValueDictionary()
value = Node()
values["temporary"] = value
del value
gc.collect()

print(f"implementation={sys.implementation.name}")
print(f"cycle-collected={cycle_ref() is None}")
print(f"finalizer={finalizer_events[0] if not finalizer.alive else 'pending'}")
print(f"weak-value-missing={'temporary' not in values}")
print(f"gc-enabled={gc.isenabled()}")
implementation=cpython
cycle-collected=True
finalizer=finalized
weak-value-missing=True
gc-enabled=True

该输出只证明这个 CPython 3.14.7 隔离进程在显式收集后已经无法通过弱引用取得该循环;它不承诺自动收集的时间、顺序或数量。分代与阈值是 CPython gc 的实现策略,既不应成为跨实现的语义,也不应取代显式资源退出。

__del__、终结与对象复活

del name 移除的是名称绑定,不是直接销毁对象的命令;若还存在其他强引用,对象继续可达。__del__ 是终结钩子,不是可靠的析构器:它的运行时机不保证,解释器关闭期间模块全局可能已被清空,钩子抛出的异常也无法让调用方在原控制流中处理。

终结代码还可能通过把 self 保存到外部位置使对象复活,使回收与后续状态更难推理。不要在 __del__ 中实现必须完成的业务清理,也不要依赖解释器关闭时的执行顺序;将确定性释放放在上下文管理器或显式 close() 中。

class Session:
def close(self) -> None:
release_transport()

这里的 close() 是调用方可安排、可捕获错误并可测试的协议;它不等待 GC,也不假定 __del__ 会在某个特定时刻执行。

weakref.ref 基础

弱引用不会让 referent 保持存活;当只剩弱引用时,垃圾收集器可以回收对象。调用 weakref.ref 对象会在 referent 仍存活时返回对象,否则返回 None。不是每种对象都支持弱引用;普通用户定义类实例通常支持,带 __slots__ 的类需要包含 __weakref__

import weakref

ref = weakref.ref(cache_entry)
entry = ref()
if entry is not None:
use(entry)

取得后立即把结果保存为强引用,再使用它;不要先单独检查存活、后在另一步重新调用弱引用,这在并发情形中会留下竞态窗口。

弱引用回调

可以给 weakref.ref 注册回调,回调会收到弱引用对象本身。弱引用回调不能安全地重新取得 referent:回调发生时 referent 已不再可用,调用该弱引用会得到 None。回调抛出的异常只会按不可传播的未处理异常方式报告,不能作为可靠的业务错误通道。

def discarded(reference) -> None:
# reference() 已不能取得原对象
record_discard()

回调适合轻量通知或维护不拥有对象的索引;不要让其依赖目标状态、关闭关键资源或决定事务结果。

弱引用容器

WeakKeyDictionary 弱持有键,WeakValueDictionary 弱持有值,WeakSet 弱持有元素。当对应对象不再有强引用时,容器条目会消失;这使缓存和附加元数据不会仅因容器本身而延长对象生命周期。

import weakref

cache = weakref.WeakValueDictionary()
cache["preview"] = preview

容器并不拥有这些对象。读取结果时仍须接受条目已经消失;需要稳定保存或明确释放的对象应由清晰的强引用所有者管理。

weakref.finalize

weakref.finalize 把独立回调登记为目标被收集时的通知,并以 alive 表示它是否仍可调用。它比原始弱引用回调更便于保留终结器自身,但仍不能变成确定性清理:调用时间取决于回收,进程退出时的行为也不应承载关键协议。

weakref.finalize 的回调函数、参数和关键字参数不能直接或间接持有目标对象;否则目标永远不能因该终结器而被回收。特别不要把目标的绑定方法作为回调。

finalizer = weakref.finalize(target, audit_log.append, "target released")

此处回调仅持有 audit_log 与字符串,而不持有 target。真正需要按时关闭的资源仍应由 withfinally 或显式方法负责。

gc 诊断与显式回收

gc.collect() 可在测试或诊断中请求一次完整收集;其返回值是收集与不可收集对象数的总和,但该数字会随周围对象和运行环境改变,不适合作为稳定断言或日志协议。若收集器正在收集时再次调用,效果未定义。

import gc

gc.collect()
assert probe() is None

gc.get_referrers()gc.get_referents() 用于调试而非日常所有权判断:返回的对象可能处于构造或循环状态。诊断时观察布尔性质或明确的业务标记,不打印地址、收集数量、回收耗时或依赖回调顺序。

终结顺序与显式资源清理

GC 不是确定性的资源管理机制。用 del 代替 close()、把离开作用域当作释放点,或用 __del__ 关闭必须及时释放的资源,都把不确定的回收时机误写成了资源合同。解释器退出时的终结顺序也不受应用代码控制。弱引用回调抛出的异常无法向调用方传播,因此它同样不是可恢复错误的处理边界。

with open("report.txt") as stream:
write_report(stream)

with 把资源释放放到可见的退出路径上,即使块内抛出异常,清理仍可由协议触发。对象内存最终何时回收可以交给运行时,资源生命周期则必须由拥有者在程序控制流中明确表达。