Python 弱引用 weakref

本文最后更新于:2022年5月21日 凌晨

Python 引用的使用量特别多,但引用使用不慎很可能影响垃圾对象回收,这时就需要弱引用解决类似问题。

背景

  • 垃圾回收

    和许多其它的高级语言一样,Python使用了垃圾回收器来自动销毁那些不再使用的对象。每个对象都有一个引用计数,当这个引用计数为0时Python能够安全地销毁这个对象。

  • 引用计数

    引用计数会记录给定对象的引用个数,并在引用个数为零时收集该对象。由于一次仅能有一个对象被回收,引用计数无法回收循环引用的对象。

  • 循环引用问题

    一组相互引用的对象若没有被其它对象直接引用,并且不可访问,则会永久存活下来。一个应用程序如果持续地产生这种不可访问的对象群组,就会发生内存泄漏。

  • 弱引用的存在价值

    在对象群组内部使用弱引用(即不会在引用计数中被计数的引用)有时能避免出现引用环,因此弱引用可用于解决循环引用的问题。

  • 弱引用的创建

    使用weakref模块,你可以创建到对象的弱引用,Python在对象的引用计数为0或只存在对象的弱引用时将回收这个对象。

弱引用

  • 官方文档

  • weakref 模块允许 Python 程序员创建对对象的弱引用。

  • 对象的弱引用不足以使对象保持活动状态:当对所指对象的唯一剩余引用是弱引用时,垃圾收集可以自由地销毁所指对象并将其内存用于其他用途。然而,在对象被实际销毁之前,即使没有强引用,弱引用也可能会返回该对象。

  • 弱引用的主要用途是实现包含大对象的缓存或映射,其中不希望大对象仅仅因为它出现在缓存或映射中而保持活动状态。

  • 例如,如果有许多大型二进制图像对象,您可能希望为每个对象关联一个名称。如果您使用 Python 字典将名称映射到图像,或将图像映射到名称,则图像对象将保持活动状态,只是因为它们在字典中显示为值或键。

  • weakref 模块提供的 WeakKeyDictionary 和 WeakValueDictionary 类是另一种选择,它们使用弱引用来构造映射,这些映射不会仅仅因为它们出现在映射对象中而使对象保持活动状态。例如,如果一个图像对象是 WeakValueDictionary 中的一个值,那么当对该图像对象的最后剩余引用是弱映射持有的弱引用时,垃圾收集可以回收该对象,并且其在弱映射中的对应条目只是删除。

使用范围

  • 不是所有的对象都可以被弱引用,可以弱引用的包括类实例、用 Python(但不是 C)编写的函数、实例方法、集合、frozensets、一些文件对象、生成器、类型对象、套接字、数组、双端队列、正则表达式模式对象和代码对象的对象。

  • list 和 dict 等几种内置类型不直接支持弱引用,但可以通过子类化添加支持:

    1
    2
    3
    4
    class Dict(dict):
    pass

    obj = Dict(red=1, green=2, blue=3) # this object is weak referenceable
  • 其他内置类型,如 tuple 和 int, str,即使在子类化时也不支持弱引用。

使用方法

创建弱引用

1
2
3
weakref.ref(object[,callback])
# callback 可选的回调函数,在引用对象被删除时调用
# 此只读属性返回当前关联到弱引用的回调。如果没有回调或者弱引用的引用不再存在,则此属性的值为 None。
  • 返回对对象的弱引用。如果引用对象还活着,则可以通过调用引用对象来检索原始对象;如果引用对象不再存在,则调用引用对象将导致 None 返回。

  • 如果提供了callback而不是None,并且返回的weakref对象还活着,那么回调将在对象即将完成时被调用;弱引用对象将作为唯一参数传递给回调;所指对象将不再可用。

  • 允许为同一个对象构造许多弱引用。为每个弱引用注册的回调将从最近注册的回调调用到最旧的注册回调。

  • 回调引发的异常将在标准错误输出中注明,但不能传播;它们的处理方式与对象的 __del__() 方法引发的异常完全相同。

  • 如果对象是可散列的,则弱引用是可散列的。即使在对象被删除后,它们仍将保持其哈希值。如果仅在对象被删除后才第一次调用 hash(),则该调用将引发 TypeError。

  • 弱引用支持相等性测试,但不支持排序。如果所指对象仍然存在,则两个引用与其所指对象具有相同的相等关系(无论回调如何)。如果任一所指对象已被删除,则仅当引用对象是同一对象时引用才相等。

  • 指向同一对象的不同弱引用为同一对象,即同一个对象仅存在一个弱引用对象,重复创建的弱引用相互之间是强引用

  • 弱引用使用时需要调用方法才可以解引用使用,因此无法为原始对象赋值,即:

    1
    2
    3
    data = np.array(3)
    ref = weak.ref(data)
    # ref() += 1 # 这种写法不可以,因为函数无法赋值,弱引用仅能引用
  • 示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import sys
import numpy as np
import weakref


if __name__ == '__main__':
data = np.random.random([3,3])
# 刚刚建立对象时的引用数
print(sys.getrefcount(data)) # 此时引用数为 2
# 建立弱引用
ref = weakref.ref(data)
# 查看增加弱引用后的引用数
print(sys.getrefcount(data)) # 此时引用数仍为 2,表明弱引用不增加引用数
# 如果为弱引用对象增加强引用,引用数会增加
prox_ref = ref()
print(sys.getrefcount(data)) # 引用数为 3,不要为弱引用对象增加强引用
# 二者输出 id 相同,表明弱引用对象指向同一内存空间
print(id(ref())) # 2809935694304
print(id(data)) # 2809935694304
# 返回为 True 表明二者为同一对象
print(ref() is data) # True
# 对象本身为弱引用对象
print(ref) # <weakref at 0x0000028E3D380A40; to 'numpy.ndarray' at 0x0000028E3D3809E0>
# 类型为 弱引用
print(type(ref)) # <class 'weakref'>
# 引用对象时和原始内容一致
print(type(ref())) # <class 'numpy.ndarray'>
# 数据内容完全一样
print(ref())
print(data)

pass

创建弱代理

1
weakref.proxy(object[, callback])
  • 功能返回一个使用弱引用的代理对象。这支持在大多数上下文中使用代理,而不需要对弱引用对象进行显式解引用。返回的对象将具有 ProxyTypeCallableProxyType 类型,具体取决于对象是否可调用。不管引用的对象是什么,代理对象都是不可哈希的; 这样就避免了许多与它们基本的可变性有关的问题,并且防止它们被用作字典键。Callback 与 ref ()函数的同名参数相同。

  • 方便之处就是,在大多数情况下可以直接将代理对象当做引用对象使用

  • 坏处就是内存空间不一致,数据类型不一致,不可哈希

  • 赋值后会变成强引用,因此不可赋值

  • 示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import sys
import numpy as np
import weakref


if __name__ == '__main__':
data = np.array([1])
# 创建对象,引用数初始为 2
print(sys.getrefcount(data)) # 2
# 创建弱引用
ref = weakref.ref(data)
# 创建弱代理
pro = weakref.proxy(data)
# 此时引用数不变
print(sys.getrefcount(data)) # 2
# 弱引用与原始数据指向同一内存空间
print(id(ref()) == id(data)) # True
# 弱代理则指向不同的对象
print(id(pro) == id(data)) # False
# 代理类型为 <class 'weakproxy'>
print(type(pro)) # <class 'weakproxy'>
# 数据内容和原始数据一致
print(pro) # [1]
print(ref()) # [1]
print(data) # [1]
# 原始数据改动后,代理和引用也会随之更改
data += 1
print(pro) # [2]
print(ref()) # [2]
print(data) # [2]
# 代理数据改动并赋值后,会变为原始数据类型,也就是转换为强引用,此时引用数会增加
pro += 1
print(sys.getrefcount(data)) # 3
# 对象类型变为 <class 'numpy.ndarray'>
print(type(pro)) # <class 'numpy.ndarray'>
print(pro) # [3]
print(ref()) # [3]
# 变为强引用后二者便是同一个对象了
print(pro is data) # True
pass

获取弱引用 / 代理数量

1
weakref.getweakrefcount(object)

返回引用对象的弱引用和代理的数量。

获取引用 / 代理列表

1
weakref.getweakrefs(object)

返回引用对象的所有弱引用和代理对象的列表。

  • 示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import sys
import numpy as np
import weakref


if __name__ == '__main__':
data = np.array([1])
print(weakref.getweakrefcount(data)) # 初始弱引用数量为 0
# 创建弱引用
ref1 = weakref.ref(data)
print(weakref.getweakrefcount(data)) # 弱引用数量为 1
# 该弱引用对象的强引用数量初始为 2
print(sys.getrefcount(ref1)) # 2
# 创建同一个对象的 第二个 弱引用
ref2 = weakref.ref(data)
# 此时弱引用数量仍为 1
print(weakref.getweakrefcount(data)) # 1
# ref1 弱引用的强引用数量增加了 1
print(sys.getrefcount(ref1)) # 3
# 创建同一个对象的 第三个 弱引用
ref3 = weakref.ref(data)
# 此时弱引用数量仍为 1
print(weakref.getweakrefcount(data)) # 1
# 三个弱引用对象互为强引用,因此强引用数量均为 4
print(sys.getrefcount(ref1)) # 4
print(sys.getrefcount(ref2)) # 4
print(sys.getrefcount(ref3)) # 4
# 对象本身是同一个
print(ref1 is ref2) # True
print(ref2 is ref3) # True
# 创建弱代理
pro1 = weakref.proxy(data)
# 对象弱引用数变为 2
print(weakref.getweakrefcount(data)) # 2
# 创建第二个 若代理 对象
pro2 = weakref.proxy(data)
# 弱引用数量仍为 2
print(weakref.getweakrefcount(data)) # 2
# 弱引用为同一个对象
print(pro1 is pro2) # True
# 弱引用列表
print(weakref.getweakrefs(data))

-->
[<weakref at 0x000001CC700519A0; to 'numpy.ndarray' at 0x000001CC70051940>, <weakproxy at 0x000001CC70EDB630 to numpy.ndarray at 0x000001CC70051940>]

弱引用键值字典

1
weakref.WeakKeyDictionary([dict])

弱引用键的映射类。当不再有对键的强引用时,字典中的条目将被丢弃。这可用于将附加数据与应用程序其他部分所拥有的对象相关联,而无需向这些对象添加属性。这对于覆盖属性访问的对象特别有用。

WeakKeyDictionary 对象有一个直接公开内部引用的附加方法。引用不能保证在使用时是“活的”,所以调用引用的结果需要在使用前检查。这可以用来避免创建引用,这些引用会导致垃圾收集器将密钥保留得比需要的时间更长。

WeakKeyDictionary.keyrefs()

返回弱引用键值的迭代对象。

弱引用值字典

1
weakref.WeakValueDictionary([dict])

弱引用值的映射类。当不再存在对该值的强引用时,字典中的条目将被丢弃。

  • WeakValueDictionary 对象具有与 WeakKeyDictionarykeyrefs() 相同的方法。
WeakValueDictionary.valuerefs()

返回对值的弱引用的迭代。

弱引用集合

1
weakref.WeakSet([elements])

设置保持对其元素的弱引用的类。当不再存在对它的强引用时,将丢弃一个元素。

弱引用方法

1
weakref.WeakMethod(method)

一个自定义 ref 子类,它模拟对绑定方法的弱引用(即,在类上定义并在实例上查找的方法)。由于绑定方法是短暂的,标准的弱引用无法保持它。 WeakMethod 有特殊的代码来重新创建绑定的方法,直到对象或原始函数死亡:

1
2
3
4
5
6
7
8
9
10
11
12
class C:
... def method(self):
... print("method called!")
...
>>> c = C()
>>> r = weakref.ref(c.method)
>>> r()
>>> r = weakref.WeakMethod(c.method)
>>> r()
<bound method C.method of <__main__.C object at 0x7fc859830220>>
>>> r()()
method called!

回收回调 / 终结器

1
weakref.finalize(obj, func, /, *args, **kwargs)
  • 返回一个可调用的终结器对象,当 obj 被垃圾收集时将调用该对象。

  • 与普通的弱引用不同,终结器在引用对象被收集之前一直存在,大大简化了生命周期管理。

  • 终结器在被调用(显式或在垃圾回收时)之前被认为是活动的,之后它就死了。调用实时终结器返回评估 func(*arg, **kwargs) 的结果,而调用死终结器返回 None。

__call__()

如果 self 还活着,则将其标记为已死并返回调用 func(*args, **kwargs) 的结果。如果 self 已死,则返回 None。

detach()

如果 self 还活着,则将其标记为已死并返回元组 (obj, func, args, kwargs)。如果 self 已死,则返回 None。

peek()

如果 self 还活着,则返回元组 (obj, func, args, kwargs)。如果 self 已死,则返回 None。

alive

如果终结器处于活动状态,则该属性为 true,否则为 false。

atexit

一个可写的布尔属性,默认为真。当程序退出时,它会调用 atexit 为 true 的所有剩余实时终结器。它们按创建的相反顺序调用。

弱引用类型

1
weakref.ReferenceType

获取弱引用对象的类型对象。

弱代理类型

1
weakref.ProxyType

返回代理(非方法)数据的类型

1
weakref.CallableProxyType

返回代理(方法)数据的类型

1
weakref.ProxyTypes

包含代理的所有类型对象的序列。这可以更简单地测试一个对象是否是一个代理,而不依赖于命名这两种代理类型。

原理浅析

  • 弱引用对象通过 wr_object 字段关联被引用的对象,如上图虚线箭头所示;

  • 一个对象可以同时被多个弱引用对象关联,图中的 Data 实例对象被两个弱引用对象关联;

  • 所有关联同一个对象的弱引用,被组织成一个双向链表,链表头保存在被引用对象中,如上图实线箭头所示;

  • 当一个对象被销毁后,Python 将遍历它的弱引用链表,逐一处理:

    • 将 wr_object 字段设为 None ,弱引用对象再被调用将返回 None ,调用者便知道对象已经被销毁了;
    • 执行回调函数 wr_callback (如有);
  • 由此可见,弱引用的工作原理其实就是设计模式中的 观察者模式Observer )。当对象被销毁,它的所有弱引用对象都得到通知,并被妥善处理。

实现

对同一对象的所有弱引用,被组织成一个双向链表,链表头保存在对象中。由于能够创建弱引用的对象类型是多种多样的,很难由一个固定的结构体来表示。因此,Python 在类型对象中提供一个字段 tp_weaklistoffset ,记录弱引用链表头指针在实例对象中的偏移量。

  • 由此一来,对于任意对象 o ,只需通过 ob_type 字段找到它的类型对象 t ,再根据 t 中的 tp_weaklistoffset 字段即可找到对象 o 的弱引用链表头。
  • 我们创建弱引用时,需要调用弱引用类型对象 weakref 并将被引用对象 d 作为参数传进去。弱引用类型对象 weakref 是所有弱引用实例对象的类型,是一个全局唯一的类型对象。

Python 调用一个对象时,执行的是其类型对象中的 tp_call 函数。因此,调用弱引用类型对象 weakref 时,执行的是 weakref 的类型对象,也就是 type 的 tp_call 函数。tp_call 函数则回过头来调用 weakref 的 tp_new 和 tp_init 函数,其中 tp_new 为实例对象分配内存,而 tp_init 则负责初始化实例对象。

参考资料


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!