PEP 442 – 安全的对象终结
- 作者:
- Antoine Pitrou <solipsis at pitrou.net>
- BDFL 委托:
- Benjamin Peterson <benjamin at python.org>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2013年5月18日
- Python 版本:
- 3.4
- 发布历史:
- 2013年5月18日
- 决议:
- Python-Dev 消息
摘要
本 PEP 旨在解决对象终结化当前的局限性。目标是能够为任何对象定义和运行终结器,无论它们在对象图中的位置如何。
本 PEP 不要求对 Python 代码进行任何更改。具有现有终结器的对象将自动受益。
定义
- 引用
- 从一个对象到另一个对象的定向链接。只要源对象本身存在且引用未被清除,该引用就会使目标对象保持活动状态。
- 弱引用
- 从一个对象到另一个对象的定向链接,它不保持其目标对象的活动状态。本 PEP 侧重于非弱引用。
- 引用循环
- 对象之间定向链接的循环子图,这使得这些对象在纯引用计数方案中无法被收集。
- 循环隔离体 (CI)
- 一个独立的子图,其中没有对象从外部被引用,包含一个或多个引用循环,并且其对象仍处于可用、未损坏的状态:它们可以从各自的终结器中相互访问。
- 循环垃圾收集器 (GC)
- 能够检测循环隔离体并将其转换为循环垃圾的设备。循环垃圾中的对象最终通过引用被清除和其引用计数降至零的自然作用而被处置。
- 循环垃圾 (CT)
- 一个以前的循环隔离体,其对象已开始被 GC 清除。循环垃圾中的对象是潜在的僵尸;如果它们被 Python 代码访问,症状可能从奇怪的 AttributeError 到崩溃不等。
- 僵尸/损坏对象
- 循环垃圾的一部分对象。该术语强调该对象不安全:其传出引用可能已被清除,或者其引用的对象之一可能是僵尸。因此,它不应被任意代码(例如终结器)访问。
- 终结器
- 当对象打算被处置时调用的函数或方法。终结器可以访问该对象并释放该对象持有的任何资源(例如互斥锁或文件描述符)。一个例子是
__del__方法。 - 复活
- 终结器在 CI 中为对象创建新引用的过程。这可能作为
__del__方法的一个古怪但受支持的副作用而发生。
影响
虽然本 PEP 讨论了 CPython 特定的实现细节,但终结语义的改变预计会影响整个 Python 生态系统。特别是,本 PEP 废除了当前“具有 __del__ 方法的对象不应成为引用循环的一部分”的指南。
好处
本 PEP 的主要好处在于具有终结器的对象,例如具有 __del__ 方法的对象和具有 finally 块的生成器。当这些对象成为引用循环的一部分时,现在可以回收它们。
本 PEP 还为未来的好处铺平了道路
- 模块关闭过程可能不再需要将全局变量设置为 None。这可以解决一类众所周知的烦人问题。
本 PEP 不改变以下语义
- 陷入引用循环的弱引用。
- 具有自定义
tp_dealloc函数的 C 扩展类型。
描述
引用计数式处置
在正常的引用计数处置中,对象的终结器在对象被解除分配之前被调用。如果终结器复活了该对象,则解除分配将被中止。
但是,如果对象已经终结化,则终结器不会被调用。这可以防止我们终结僵尸(见下文)。
循环隔离体的处置
循环隔离体首先由垃圾收集器检测,然后被处置。检测阶段不变,此处不作描述。CI 的处置传统上按以下顺序进行
- 清除对 CI 对象的弱引用,并调用其回调。此时,这些对象仍然可以安全使用。
- CI 变为 CT,因为 GC 系统地破坏了其中所有已知引用(使用
tp_clear函数)。 - 无。所有 CT 对象都应该在步骤 2 中被处置(作为清除引用的副作用);此收集已完成。
本 PEP 建议将 CI 处置转换为以下序列(新步骤为粗体)
- 清除对 CI 对象的弱引用,并调用其回调。此时,这些对象仍然可以安全使用。
- 调用所有 CI 对象的终结器。
- 再次遍历 CI 以确定它是否仍然隔离。如果确定 CI 中至少有一个对象现在可以从 CI 外部访问,则此收集被中止,并且整个 CI 被复活。否则,继续。
- CI 变为 CT,因为 GC 系统地破坏了其中所有已知引用(使用
tp_clear函数)。 - 无。所有 CT 对象都应该在步骤 4 中被处置(作为清除引用的副作用);此收集已完成。
注意
GC 在上述步骤 2 之后不重新计算 CI,因此需要步骤 3 来检查整个子图是否仍然隔离。
C级别更改
类型对象获得一个新的 tp_finalize 槽,__del__ 方法被映射到该槽(反之亦然)。生成器被修改为使用此槽,而不是 tp_del。 tp_finalize 函数是一个普通的 C 函数,它将以一个有效且活动的 PyObject 作为其唯一参数被调用。它不需要操作对象的引用计数,因为这将由调用者完成。但是,它必须确保在返回调用者之前恢复原始异常状态。
为了兼容性,tp_del 保留在类型结构中。处理具有非 NULL tp_del 的对象保持不变:当它们是 CI 的一部分时,它们不会被终结,并最终进入 gc.garbage。但是,CPython 源代码树中不再遇到非 NULL tp_del(除了用于测试目的)。
提供了两个新的 C API 函数,以方便调用 tp_finalize,尤其是从自定义解除分配器中。
在内部方面,GC 头中为 GC 管理的对象保留了一个位,以指示它们已终结。这有助于避免两次终结一个对象(特别是,在 GC 破坏 CT 对象之后终结它)。
注意
未启用 GC 的对象也可以有一个 tp_finalize 槽。它们不需要额外的位,因为它们的 tp_finalize 函数只能从解除分配器中调用:因此,除非复活,否则不能两次调用它。
讨论
可预测性
按照这个方案,对象的终结器总是被调用一次,即使它后来被复活了。
对于 CI 对象,终结器被调用的顺序(上述步骤 2)是未定义的。
安全性
解释为什么提议的更改是安全的非常重要。有两个方面需要讨论
- 终结器可以访问僵尸对象(包括正在终结的对象)吗?
- 如果终结器改变了对象图以影响 CI 会发生什么?
让我们讨论第一个问题。我们将可能的案例分为两类
- 如果正在终结的对象是 CI 的一部分:根据构造,CI 中的对象尚未成为僵尸,因为 CI 终结器在任何引用破坏完成之前被调用。因此,终结器无法访问不存在的僵尸对象。
- 如果正在终结的对象不是 CI/CT 的一部分:根据定义,CI/CT 中的对象没有从 CI/CT 外部指向它们的任何引用。因此,终结器无法到达任何僵尸对象(即使正在终结的对象本身被僵尸对象引用)。
现在讨论第二个问题。有三种潜在情况
- 终结器清除了对 CI 对象的现有引用。CI 对象可能在 GC 尝试破坏它之前被处置,这很好(GC 只需要意识到这种可能性)。
- 终结器创建了对 CI 对象的新引用。这只能从 CI 对象的终结器中发生(见上文)。因此,在所有 CI 终结器被调用后(上述步骤 3),新引用将被 GC 检测到,并且收集将被中止,没有任何对象被破坏。
- 终结器清除或创建对非 CI 对象的引用。根据构造,这不是问题。
实施
在存储库的 finalize 分支中提供了实现,地址为 http://hg.python.org/features/finalize/。
验证
除了运行正常的 Python 测试套件外,该实现还增加了针对各种终结可能性的测试用例,包括引用循环、对象复活和旧版 tp_del 槽。
该实现还经过检查,以确保在以下测试套件上不会产生任何回归
- Tulip,它大量使用了生成器
- Tornado
- SQLAlchemy
- Django
- zope.interface
参考资料
关于引用循环收集和弱引用回调的说明:http://hg.python.org/cpython/file/4e687d53b645/Modules/gc_weakref.txt
生成器内存泄漏:http://bugs.python.org/issue17468
允许对象决定它们是否可以被 GC 收集:http://bugs.python.org/issue9141
基于 GC 的模块关闭过程 http://bugs.python.org/issue812369
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0442.rst