PEP 442 – 安全对象终结
- 作者:
- Antoine Pitrou <solipsis at pitrou.net>
- BDFL-代表:
- Benjamin Peterson <benjamin at python.org>
- 状态:
- 最终
- 类型:
- 标准跟踪
- 创建:
- 2013-05-18
- Python 版本:
- 3.4
- 历史记录:
- 2013-05-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 对象的弱引用被清除,它们的回调被调用。此时,这些对象仍然可以安全使用。
- 当 GC 系统地破坏 CI 中的所有已知引用(使用
tp_clear
函数)时,CI 变为 CT。 - 什么都没有。所有 CT 对象都应该在步骤 2 中被处理(作为清除引用的副作用);此收集已完成。
本 PEP 提出将 CI 处理转换为以下序列(新步骤以粗体显示)
- 对 CI 对象的弱引用被清除,它们的回调被调用。此时,这些对象仍然可以安全使用。
- 调用所有 CI 对象的终结器。
- 再次遍历 CI 以确定它是否仍然是隔离的。如果确定 CI 中至少有一个对象现在可以从 CI 外部访问,则此收集将中止,整个 CI 将被复活。否则,继续。
- 当 GC 系统地破坏 CI 中的所有已知引用(使用
tp_clear
函数)时,CI 变为 CT。 - 什么都没有。所有 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 管理的对象保留了一个位,以指示它们是否已被终结。这有助于避免两次终结对象(尤其是,在 CT 对象被 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 终结器被调用后被 GC 检测到(上面的步骤 3),并且收集将中止,没有任何对象被破坏。
- 终结器清除或创建一个对非 CI 对象的引用。通过构造,这不是问题。
实现
实现可以在 http://hg.python.org/features/finalize/ 处的存储库的 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