Python 的弱引用学习

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

引用计数会记录给定对象的引用个数,并在引用个数为零时收集该对象。由于一次仅能有一个对象被回收,引用计数无法回收循环引用的对象,形成内存泄露。解决方法是有一套专门用于处理循环引用的垃圾回收器,gc

弱引用,与强引用相对,其在保留引用的前提下,不增加计数,也不组织目标被回收。弱引用的主要作用就是减少循环引用,减少内存中不必要的对象存在的数量。

注意:不是所有类型都支持弱引用,比如 int、tuple 等。

References

简单的创建弱引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import weakref
import sys
class X:
def __del__(self):
print(id(self), 'dead.')
a = X()
w = weakref.ref(a) # 创建弱引用
print('a:', a)
print('w:', w)
print('w():', w())
ptint(w() is a) # 两者是否引用同一对象
print(sys.getrefcount(a)) # 得到目标对象的引用计数
print('deleting a')
del a # 解除目标对象名字引用,对象被回收
print('w():', w())

结果:

1
2
3
4
5
6
7
8
a: <__main__.X object at 0x1082a6828>
w: <weakref at 0x1082a0e08; to 'X' at 0x1082a6828>
w(): <__main__.X object at 0x1082a6828>
True
2 ## 弱引用没有增加目标对象引用计数
deleting a
4431964200 dead.
w(): None ## 弱引用失效

Reference Callbacks

弱引用可用于一些特定场合,比如缓存、监控等。这类“外挂”场景不应该影响目标对象,不能阻止它们被回收。弱引用的另一个典型应用是实现Finalizer,也就是在对象被回收时执行额外的“清理操作”。

1
2
3
4
5
6
7
8
9
10
>>> a = X()
>>> def callback(w):
print(w, w() is None)
>>> w = weakref.ref(a, callback)
>>> del a
4431964200 dead.
<weakref at 0x1082a0e58; dead> True

callback回调函数在引用对象被删除时被调用,回调函数参数为弱引用而非目标对象。因为回调函数执行时,目标已无法访问

Proxies

抛开对生命周期的影响不说,弱引用与普通名字的最大区别在于其类函数的调用语法。不过可用 proxy 改进,使其和名字引用语法一致。

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
>>> a = X()
>>> a.name = "buzz"
>>> w = weakref.ref(a)
>>> w.name
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'weakref' object has no attribute 'name'
>>> w().name # 必须使用调用语法
'buzz'
>>> p = weakref.proxy(a)
>>> p
<weakproxy at 0x1082a0ea8 to X at 0x1082a6828>
>>> p.name
'buzz'
>>> p.age = 60
>>> a.age
60
>>> del a
4431964200 dead.

gc

引用计数机制实现简单,能实时响应,在计数归零时立即清理该对象所占的内存,绝大多数时候都能高效运作。但当两个或更多对象形成循环引用时,就不行了,需要用另一套专门用于处理循环引用的垃圾回收器,gc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> class X:
... def __del__(self):
... print(self, 'dead.')
...
>>> import gc
>>> gc.disable() # 关闭 gc
>>> a = X()
>>> b = X()
>>> a.x = b # 构成循环引用
>>> b.x = a
>>> del a
>>> del b # 删除全部名字后,对象未被回收,所以引用计数失效
>>> gc.enable() # 重新打开 gc
>>> gc.collect() # 主动启动一次回收操作,循环引用对象被正确回收
<__main__.X object at 0x106ae97f0> dead.
<__main__.X object at 0x106ae9978> dead.

在解释器启动时,gc 默认是打开的,并且追踪所有可能造成循环引用的对象。相比于引用计数,它是一种延迟回收机制,只有当内部预设的阈值条件满足时,才会在后台启动。当然,也可以强制执行回收,但是不建议频繁使用。

对于某些性能优先的算法,在确保没有循环引用的前提下,临时关闭 gc 可以获得更好的性能。

在做性能测试(timeit)时,会关闭 gc, 避免垃圾回收对执行计时造成影响。

注意

Ipython 对于弱引用和垃圾回收存在干扰,我用 Ipython 运行测试上面所有代码时结果都是错误的,所以最好用原生环境或者文件模式运行代码。