PEP 205 – 弱引用
- 作者:
- Fred L. Drake, Jr. <fred at fdrake.net>
- 状态:
- 最终版
- 类型:
- 标准轨迹
- 创建:
- 2000年7月14日
- Python 版本:
- 2.1
- 发布历史:
- 2001年1月11日
动机
Python 程序员已经注意到弱引用的两种基本应用:对象缓存和减少循环引用带来的痛苦。
缓存(弱字典)
需要允许维护表示外部状态的对象,将单个实例映射到外部现实,如果允许将多个实例映射到同一个外部资源,则会造成在实例之间维护同步的不必要的困难。在这些情况下,一个常见的习惯用法是支持实例缓存;工厂函数用于返回新的或现有的实例。
这种方法的难点在于必须容忍以下两种情况之一:缓存无限增长,或者需要在应用程序的其他地方显式管理缓存。后者可能非常乏味,并且会导致比解决手头问题真正需要的代码更多,而前者对于长时间运行的进程甚至内存需求很大的相对较短的进程来说都是不可接受的。
- 需要由单个实例表示的外部对象,无论有多少内部用户。这对于表示需要整体写入磁盘的文件很有用,而不是为每次使用都锁定和修改。
- 创建成本高昂但可能被多个内部使用者需要的对象。类似于第一种情况,但不一定绑定到外部资源,并且可能不是共享状态的问题。只有在某种“软”引用或单个对象的使用者生命周期重叠的可能性很高的情况下,弱引用在这种情况下才有用。
循环引用
- DOM 需要大量的循环(到父级和文档节点)引用,但这些可以通过使用从每个节点到其父级的弱字典映射来消除。这在诸如
xml.dom.pulldom
之类的内容的上下文中可能特别有用,允许.unlink()
操作成为一个空操作。
本提案分为以下几个部分
- 提议的解决方案
- 实现策略
- 可能的应用
- Python 中之前的弱引用工作
- Java 中的弱引用
由于一个早期提案的全文似乎在网络上不可用,因此将其作为附录包含在内。
解决方案空间的各个方面
弱引用问题有两个不同的方面
- 弱引用的失效
- 弱引用在 Python 代码中的表示
失效
过去,弱引用失效的方法通常依赖于存储强引用并能够检查所有弱引用对象实例,并在其被引用者的引用计数变为 1 时使其失效(表示弱引用存储的引用是最后一个剩余的引用)。这样做的优点是 Python 中的内存管理机制不需要改变,并且任何类型都可以被弱引用。
这种失效方法的缺点是,它假设弱引用的管理被调用得足够频繁,以便在合理的时间范围内注意到弱引用的对象;由于这意味着扫描某些数据结构以使引用失效,这是一个关于弱引用对象数量的 O(N) 操作,因此对于任何单个被弱引用的对象来说,这并没有得到有效地摊销。这也假设应用程序正在调用处理弱引用对象的代码,并且具有一定的频率,这使得弱引用对库代码的吸引力降低。
另一种失效方法是使释放代码意识到弱引用的可能性,并在每次对象被释放时,对弱引用管理代码进行特定调用以全部失效。这需要更改弱引用对象的 tp_dealloc 处理程序;对于支持弱引用的对象,在处理程序的“顶部”需要额外的调用,并且还需要一种有效的方法来从对象映射到该对象的弱引用链。
表示
弱引用在 Python 层的表示方式有两种:作为显式引用对象,需要在其上执行某些操作才能检索到对底层对象的可用引用,以及尽可能地伪装成原始对象的代理对象。
当在 Python 中添加一些额外的对象管理层时,引用对象易于使用;可以显式地检查引用是否存活,而无需对被引用者调用操作并捕获在使用无效弱引用时引发的某些特殊异常。
然而,许多用户更喜欢代理方法,仅仅是因为弱引用看起来非常像原始对象。
提议的解决方案
弱引用应该能够指向任何可能具有大量内存大小(直接或间接)的 Python 对象,或者持有对外部资源(数据库连接、打开的文件等)的引用。
一个新的模块 weakref 将包含用于创建弱引用的新函数。 weakref.ref()
将创建一个“弱引用对象”,并可以选择附加一个回调,该回调将在对象即将被终结时被调用。 weakref.mapping()
将创建一个“弱字典”。第三个函数 weakref.proxy()
将创建一个代理对象,该对象的行为有点像原始对象。
弱引用对象允许访问引用的对象(如果它还没有被回收)并确定该对象是否仍然存在于内存中。通过调用引用对象来检索被引用者。如果被引用者不再存活,则将返回 None。
弱字典将任意键映射到值,但不拥有对值的引用。当值被终结时,将其作为值的 (键,值) 对将从包含此类对的所有映射中删除。与字典类似,弱字典不可哈希。
代理对象是试图尽可能地表现得像它们代理的对象的弱引用。无论底层类型是什么,代理都不可哈希,因为它们充当弱引用的能力依赖于会导致在用作字典键时出现故障的基本可变性——即使在被引用者死亡之前计算了正确的哈希值,生成的代理也不能用作字典键,因为它在被引用者过期后无法进行比较,而可比性对于字典键是必要的。在被引用者死亡后对代理对象执行的操作会在大多数情况下导致引发 weakref.ReferenceError。“is”比较、 type()
和 id()
将继续工作,但始终引用代理而不是被引用者。
向弱引用注册的回调必须接受一个参数,该参数将是弱引用或代理对象本身。在回调中无法访问或恢复该对象。
实现策略
弱引用的实现将包括一个引用容器列表,这些容器必须为每个可弱引用的对象清除。如果引用来自弱字典,则首先清除字典条目。然后,使用作为参数传递的对象调用任何关联的回调。一旦所有回调都被调用,对象就会被终结并释放。
许多内置类型将参与弱引用管理,任何扩展类型都可以选择这样做。类型结构将包含一个附加字段,该字段提供实例结构中的偏移量,该偏移量包含弱引用结构列表。如果字段的值 <= 0,则对象不参与。在这种情况下, weakref.ref()
、 <weakdict>.__setitem__()
和 .setdefault()
以及项目赋值将引发 TypeError
。如果字段的值 > 0,则可以生成新的弱引用并将其添加到列表中。
采用这种方法是为了允许任意扩展类型参与,而不会对数字或其他小型类型造成内存开销。
支持弱引用的标准类型包括实例、函数以及绑定和未绑定方法。随着 Python 2.2 中类类型的(“新式类”)的添加,类型增加了对弱引用的支持。如果类类型实例具有可弱引用的基类型,则它们是可弱引用的,类没有指定 __slots__
,或者某个插槽名为 __weakref__
。生成器也支持弱引用。
可能的应用
PyGTK+ 绑定?
Tkinter – 可以通过使用从部件到其父级的弱引用来避免循环引用。在典型情况下,对象不会更快地被丢弃,但程序员在释放引用之前调用 .destroy()
的依赖性不会那么强。这主要有利于长时间运行的应用程序。
DOM 树。
Python 中之前的弱引用工作
Dianne Hackborn 提出了一种名为“虚拟引用”的东西。“vref”对象非常类似于 java.lang.ref.WeakReference 对象,只是没有等效的失效队列。实现“弱字典”与在 Java 中仅使用弱引用(没有失效队列)一样困难。关于此的信息已从网络上消失,但以下作为附录包含在内。
Marc-André Lemburg 的 mx.Proxy 包
Dieter Maurer 的 weakdict 模块是用 C 和 Python 实现的。它似乎自 Python 1.5.2a 以来就没有更新过网页,所以我还不确定该实现是否与 Python 2.0 兼容。
Alex Shindich 的 PyWeakReference
Eric Tiedemann 有一个弱字典实现
Java 中的弱引用
http://java.sun.com/j2se/1.3/docs/api/java/lang/ref/package-summary.html
Java 提供了三种形式的弱引用,以及一个有趣的辅助类。这三种形式称为“弱”、“软”和“幽灵”引用。相关类定义在 java.lang.ref 包中。
对于每种引用类型,当它被内存分配器使无效时,可以选择将其添加到队列中。此功能的主要目的是,它允许组合更大的结构以包含弱引用语义,而不必施加大量的额外锁定要求。例如,使用此功能创建“弱”哈希表并不困难,当不再在其他地方使用引用时,该哈希表会删除键和被引用者。如果存储对象的释放不频繁,则使用弱引用表示对象而没有某种失效通知队列会导致对哈希表上所需的各种操作进行更加乏味的实现。这可能是性能瓶颈。
Java 的“弱”引用与 Dianne Hackborn 早期的 vref 提案最为相似:一个引用对象指向单个 Python 对象,但并不拥有对该对象的引用。当该对象被释放时,引用对象将失效。引用对象的使用者可以轻松地确定引用是否已失效,或者在尝试使用被引用对象时引发 NullObjectDereferenceError。
“软”引用与此类似,但不会在所有其他对被引用对象的引用都被释放后立即失效。“软”引用确实拥有一个引用,但允许内存分配器在其他地方需要内存时释放被引用对象。目前尚不清楚这是否意味着软引用会在 malloc()
实现调用 sbrk()
或其等效函数之前释放,或者软引用仅在 malloc()
返回 NULL
时才会被清除。
“虚幻”引用略有不同;与弱引用和软引用不同,在将被引用对象添加到其队列时,不会清除被引用对象。当某个对象的全部虚幻引用都被移出队列时,该对象才会被清除。这可以用于使对象保持存活,直到执行一些额外的清理工作,这些工作需要在对象的 .finalize()
方法被调用之前完成。
与其他两种引用类型不同,“虚幻”引用必须与一个失效队列相关联。
附录 – Dianne Hackborn 的 vref 提案(1995)
[此内容已缩进并重新调整段落,但内容没有更改。–Fred]
提案:虚拟引用
为了尝试部分解决关于引用计数与垃圾回收的反复讨论,我想提出对 Python 的一个扩展,这将有助于创建“结构良好”的循环图。特别是,它应该允许至少创建带有父级反向指针的树和双向链表,而无需担心循环。
我想提出的基本机制是“虚拟引用”,或简称“vref”。vref 本质上是对一个对象的句柄,它不会增加对象的引用计数。这意味着持有对对象的 vref 不会阻止该对象被销毁。这将允许 Python 程序员例如创建上述树结构,该结构在不再使用时会自动销毁 - 通过将所有父级反向引用都设为 vref,它们不再创建阻止树被销毁的引用循环。
为了实现此机制,Python 核心必须确保永远不会留下任何 -真实- 指针来引用不再存在的对象。我想提出的实现涉及对当前 Python 系统的两个基本添加
- 一种新的“vref”类型,通过它 Python 程序员可以创建和操作虚拟引用。在内部,它基本上是一个 C 级的 Python 对象,带有指向它所引用的 Python 对象的指针。但是,与所有其他 Python 代码不同,它不会更改此对象的引用计数。此外,它包含两个指针来实现一个双向链表,该链表在下面使用。
- 在基本 Python 对象[
PyObject_Head
in object.h]中添加一个新字段,该字段要么为NULL
,要么指向引用它的所有 vref 对象列表的头部。当一个 vref 对象附加到另一个对象时,它会将自己添加到此链表中。然后,如果任何 vref 存在于其上的对象被释放,它可能会遍历此列表并确保其上的所有 vref 都指向某个安全值,例如 Nothing。
希望此实现对当前 Python 核心产生的影响最小 - 当不存在 vref 时,它应该只向所有对象添加一个指针,并在每次对象被释放时检查一个 NULL
指针。
回到 Python 语言级别,我考虑了 vref 对象的两种可能的语义 -
指针语义
在此模型中,vref 的行为本质上类似于 Python 级的指针;Python 程序必须显式地取消引用 vref 以操作它所引用的实际对象。
使用此模型的示例 vref 模块可以包含函数“new”;当用作‘MyVref = vref.new(MyObject)’时,它返回一个新的 vref 对象,使得 MyVref.object == MyObject
。如果 MyObject
曾经被释放,则 MyVref.object
将更改为 Nothing。
举一个具体的例子,我们可以引入一些新的 C 样式语法
&
– 一元运算符,在对象上创建 vref,与vref.new()
相同。*
– 一元运算符,取消引用 vref,与VrefObject.object
相同。
然后我们可以定义
1. type(&MyObject) == vref.VrefType
2. *(&MyObject) == MyObject
3. (*(&MyObject)).attr == MyObject.attr
4. &&MyObject == Nothing
5. *MyObject -> exception
规则 #4 很微妙,但它之所以出现是因为我们对(没有真实引用的 vref)创建了一个 vref。因此,当内部 vref 不可避免地消失时,外部 vref 将被清除为 Nothing。
代理语义
在此模型中,Python 程序员操作 vref 对象的方式就像操作它所引用的对象一样。这是通过实现 vref 来完成的,以便对其进行的所有操作都重定向到其引用的对象。使用此模型,取消引用运算符 (*) 变得毫无意义;相反,我们只有引用运算符 (&),并定义
1. type(&MyObject) == type(MyObject)
2. &MyObject == MyObject
3. (&MyObject).attr == MyObject.attr
4. &&MyObject == MyObject
同样,规则 #4 非常重要 - 在这里,外部 vref 实际上是对原始对象的引用,而不是内部 vref。这是因为应用于 vref 的所有操作实际上都应用于其对象,因此创建 vref 的 vref 实际上会导致创建后者对象的 vref。
第一个指针语义的优点是它很容易实现;vref 类型非常简单,至少需要一个属性 object 和一个创建引用的函数。
但是,我真的很喜欢代理语义。它不仅减少了 Python 程序员的负担,而且允许您执行诸如在任何使用实际对象的地方使用 vref 之类的好事。不幸的是,在当前的 Python 实现中,实现它可能非常痛苦,甚至实际上不可能。不过,我确实有一些关于如何做到这一点的想法,如果它看起来很有趣;一种可能性是引入新的类型检查函数来处理 vref。这将希望较旧的 C 模块(不希望 vref 简单地返回类型错误)能够修复,直到它们可以被修复为止。
最后,此系统可以提供一些其他附加功能。对我来说,一个特别有趣的功能涉及允许 Python 程序员向 vref 添加“析构函数” - 此 Python 函数将在被引用对象被释放之前立即被调用,允许 Python 程序隐式地附加到另一个对象并观察它是否消失。这看起来很不错,尽管我还没有想出任何实际用途…… :)
– Dianne
版权
本文档已归入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0205.rst
上次修改时间:2023-09-09 17:39:29 GMT