本篇文章为大家介绍Python中弱引用的概念。
我们知道,任何对象在程序运行时都是保存在内存中的。由于内存是有限的,Python需要及时去清理一些不会再使用的对象并释放它们所占用的内存。那么,什么是不会再使用的对象呢?Python怎么知道谁不会再被使用呢?
我们在交互环境下尝试如下:
>>>a = [1, 2, 3]
>>>del a
>>>a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
可以看到,当采用del
时,a
被删掉了。自然地,a
所引用的对象[1, 2, 3]
再也找不到了,它会被Python删除并释放掉内存。那么是不是使用del
就可以真正删掉对象呢?NO:
>>>a = [1, 2, 3]
>>>b = a
>>>id(a), id(b)
(140197895626824, 140197895626824)
>>>del a
>>>a
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined
>>>b, id(b)
([1, 2, 3], 140197895626824)
从结果可以看到,a
和b
引用的同样的对象[1, 2, 3]
,然而del a
后,真正的[1, 2, 3]
并没有被删除,通过b
还可以访问到它。这便是引用计数起到的作用。在Python中,每一个对象在创建时,都有一个默认的变量存储着该对象的引用计数,即,该对象被其他人引用了多少次。上例中,a = [1, 2, 3]
时,对象[1, 2, 3]
被引用了一次,而b = a
使得对象[1, 2, 3]
被引用了两次。后边del a
时,对象[1, 2, 3]
引用计数减一。当引用计数减为0时,Python会启动回收过程,将对象[1, 2, 3]
释放。所以,del
仅仅会删掉标识符,并使引用计数减一,而并非真正删除对象。
想要获取引用计数值,需要使用sys
下的getrefcount
方法。需要说明的是,getrefcount
自身会对对象做一次临时引用,因而得到的结果会大1:
>>>import sys
>>>a = [1, 2, 3]
>>>sys.getrefcount(a)
2
>>>b = a
>>>sys.getrefcount(a)
3
>>>del a
>>>sys.getrefcount(b)
2
下面来看一个有趣的事(如果你还记得__del__
方法的话):
>>>class A: pass
>>>A.__del__ = lambda self: print('Set free...')
>>>a = A()
>>>b = a
>>>del a
这里会发生什么?答案是什么都不会发生。这说明了特殊方法__del__
并非与del
完全绑定的,它实际上仅仅会在对象真正被删除那一刻前会被调用,而前面我们看到了,del
并不会真正删除对象,仅当引用计数降为0时,对象才会被Python(严格地说是CPython)删除。知道了这一点,我们接着来看:
>>>b
<__main__.A object at 0x7f8f78403f98>
>>>del b
这里会发生什么?答案是什么都不会发生。实际上这是由Python shell的一个小特性导致的,它会将上一次的结果记录在标识符_
下。所以当我们前面看了一眼b
的时候,_
已经指向了b
所引用的对象:
>>>_
<__main__.A object at 0x7f8f78403f98>
我们可以再随意输出一个结果来替换_
,这样b
所引用的对象就会被释放了:
>>>2
Set free...
2
直接del b
就会直接删除对象:
>>>a = A()
>>>b = a
>>>del a
>>>del b
Set free...
Python中还存在另一个函数,允许我们在某个对象被删除前的最后一刻做一些“挣扎”,即模块weakref
中的finalize
方法。它可以为对象注册一个函数,并返回一个finalize
对象追踪目标对象的生命周期。在对象被释放前的最后一刻,Python会调用注册的函数:
>>>import weakref
>>>a = A()
>>>def free(): print('Set free after free()...')
>>>b = weakref.finalize(a, free)
>>>b
<finalize object at 0x7f8f795de100; for 'A' at 0x7f8f78403f28>
我们还可以通过finalize
的alive
属性查看对象的状态。来看看删除a
会发生什么:
>>>b.alive
True
>>>del a
Set free...
Set free after free()...
>>>b
<finalize object at 0x7f8f795de100; dead>
>>>b.alive
False
可以看到,a
已“死去”。
这里存在一个问题,a
被传入函数中,并通过finalize
对象b
进行了追踪,为什么可以直接删除呢?因为b
采用弱引用引用了a
。
所谓弱引用,是一种并不会增加引用计数值的引用。这样,弱引用不会影响对象被回收的过程。创建一般的弱引用采用weakref.ref
或weakref.proxy
:
>>>import weakref, sys
>>>a = {1, 2, 3}
>>>b = weakref.ref(a)
>>>c = weakref.proxy(a)
>>>d = a
>>>e = a.copy()
>>>f = [4, 5, a]
>>>sys.getrefcount(a)
根据前面的描述,上述结果应该是多少呢?请自己尝试一下。
我们重点来看b
和c
。
>>>b
<weakref at 0x7f8f7840d228; to 'set' at 0x7f8f783f1c88>
>>>c
<weakproxy at 0x7f8f7840d278 to set at 0x7f8f783f1c88>
两者都是以弱引用的方式引用了目标对象。这里采用集合set
的原因是list
和dict
都不支持直接弱引用:
>>>g = [1, 2, 3]
>>>h = weakref.ref(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: cannot create weak reference to 'list' object
我们可以通过弱引用或弱代理直接访问对象,区别在于弱引用需要调用一次才返回对象的引用。
>>>b()
{1, 2, 3}
>>>print(c)
{1, 2, 3}
当弱引用指示的目标(英文称作referent)被回收以后,弱引用将返回None
,而弱代理将抛出异常:
>>>del a, d, e, f
>>>b
<weakref at 0x7f8f7840d228; dead>
>>>c
<weakproxy at 0x7f8f7840d278 to NoneType at 0x8a26a0>
>>>b()
>>>print(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ReferenceError: weakly-referenced object no longer exists
两者还支持注册回调函数,以便在指示对象被释放时调用;此外,列表或字典本身不可被弱引用,但子类可以:
>>>class List(list): pass
>>>l = List([1, 2, 3])
>>>def cb(obj): print('Callback: object {} free...'.format(obj))
>>>r = weakref.ref(l, cb)
>>>r()
[1, 2, 3]
>>>del l
>>>1
Callback: object <weakref at 0x7f8f783a2e08; dead> free...
1
为什么要打一个1?
为什么要使用弱引用呢?一个最主要的用处在于针对一些缓存中的大型数据(例如图像数据),我们可以通过弱引用来避免增加引用计数,从而使得缓存中的数据可以正常被释放,提高内存使用的效率。
最后,如果希望弱引用多个对象,可以使用weakref
中的WeakSet
,WeakKeyDictionary
和WeakValueDictionary
等数据类型。还有一个WeakMethod
类,可以对实例方法进行弱引用,下面仅给出一个示例:
>>>class A:
... def method(self):
... print('Instance method')
>>>a = A()
>>>r = weakref.WeakMethod(a.method)
>>>r()()
Instance method