Python: 哪些类型支持弱引用?

Python: which types support weak references?

代码:

from weakref import WeakSet
se = WeakSet()
se.add(1)

输出:

TypeError: cannot create weak reference to 'int' object

Doc:

Several built-in types such as list and dict do not directly support weak references but can add support through subclassing:

...

Other built-in types such as tuple and int do not support weak references even when subclassed (This is an implementation detail and may be different across various Python implementations.).

表达力不够,无法解释:


补充一些想法:

对于上面的示例,您可以将 int 包装在用户定义的包装器 class 中,并且该包装器 class 支持弱引用(熟悉 Java 的人会记得intInteger):

from weakref import WeakSet
se = WeakSet()

class Integer:
    def __init__(self, n=0):
        self.n = n

i = 1
I = Integer(1)

se.add(i)   # fail
se.add(I)   # ok

我不确定为什么 Python 不为常用的内置类型(intstr 等)提供自动包装,而是简单地说他们不支持弱引用。这可能是由于性能问题,但无法弱引用这些内置类型大大减少了它的使用。

首先:这都是 CPython 特定的。 Weakrefs 在不同的 Python 实现上有不同的工作方式。

大多数内置类型不支持弱引用,因为 Python 的弱引用机制给每个支持弱引用的对象增加了一些开销,而 Python 开发团队决定不支持' 希望大多数内置类型支付该开销。这种开销最简单的表现形式是任何具有弱引用支持的对象都需要 space 用于弱引用管理的额外指针,并且大多数内置对象不会为该指针保留 space。

尝试编译一个包含弱引用支持的所有类型的完整列表与尝试编译一个包含所有红头发人类的完整列表一样富有成效。如果要判断一个类型是否有弱引用支持,可以查看它的__weakrefoffset__,对于弱引用支持的类型是非零的:

>>> int.__weakrefoffset__
0
>>> type.__weakrefoffset__
368
>>> tuple.__weakrefoffset__
0
>>> class Foo(object):
...     pass
... 
>>> class Bar(tuple):
...     pass
... 
>>> Foo.__weakrefoffset__
24
>>> Bar.__weakrefoffset__
0

类型的 __weakrefoffset__ 是从实例开始到 weakref 指针的字节偏移量,如果实例没有 weakref 指针,则为 0。它 corresponds to the type struct's tp_weaklistoffset at C level. As of the time of this writing, __weakrefoffset__ is completely undocumented, but tp_weaklistoffset is documented,因为在 C 中实现扩展类型的人需要了解它。

没有涵盖两件事。


首先,在 2.1 版本 Python 中添加了 weakref。

对于 2.1 之后添加的所有内容(包括 objecttype),默认情况下会添加 weakref 支持,除非有充分的理由不这样做。

但是对于所有已经存在的东西,尤其是像 int 这样的小东西,再添加 4 个字节(当时大多数 Python 实现都是 32 位的,所以我们只调用指针 4字节)可能会导致为 1.6/2.0 或更早版本编写的所有 Python 代码出现明显的性能下降。因此,为这些类型添加 weakref 支持的门槛更高。


其次,Python 允许实现合并它可以证明是 immutable 的内置类型的值,对于其中一些内置类型,CPython 利用了优势那个。例如(不同版本细节有所不同,仅以此为例):

  • 从-5到255的整数、空字符串、单字符原则tableASCII字符串、空字节、单字节字节、空元组get在启动时创建的单例实例,以及大多数尝试构造一个等于这些单例之一的新值,而不是获取对单例的引用。
  • 许多字符串缓存在字符串实习生 table 中,许多尝试构造与实习生字符串具有相同值的字符串的尝试取而代之的是获取对现有字符串的引用。
  • 在一个编译单元内,编译器会将两个独立的常量(相等的整数、字符串、整数和字符串的元组等)合并为对同一常量的两个引用。

因此,这些类型的弱引用不会像您最初想象的那样有用。许多值永远不会消失,因为它们是对单例或模块常量或驻留字符串的引用。即使是那些不是不朽的,您对它们的引用也可能比您预期的要多。

当然,在某些情况下 weakrefs 还是很有用的。如果我计算十亿个大整数,其中大部分都不会永生或共享。但这意味着它们对这些类型不太有用,这在权衡使每个 int 4 字节更大的权衡时必须是一个因素,这样您就可以通过安全地释放它们来节省内存一些相对不常见的情况。