8.5 del 和垃圾回收
del 语句删除名称(对象的引用),而不是对象。del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。
在 CPython 中,垃圾回收使用的主要算法是引用计数。实际上,每个对象都会统计有多少引用指向自己。当引用计数归零时,对象立即就被销毁:CPython 会在对象上调用 __del__ 方法(如果定义了),然后释放分配给对象的内存。
CPython 2.0 增加了分代垃圾回收算法,用于检测引用循环中涉及的对象组——如果一组对象之间全是相互引用,即使再出色的引用方式也会导致组中的对象不可获取。Python 的其他实现有更复杂的垃圾回收程序,而且不依赖引用计数,这意味着,对象的引用数量为零时可能不会立即调用 __del__ 方法。
示例 8-16 为了演示对象生命结束时的情形,使用 weakref.finalize 注册一个回调函数,在销毁对象时调用。
import weakref
# 1. s1 和 s2 是别名,指向同一个集合,{1, 2, 3}。
s1 = {1, 2, 3}
s2 = s1
# 2. 这个函数一定不能是要销毁的对象的绑定方法,否则会有一个指向对象的引用。
def bye():
print('Gone ... with the wind...')
# 3. 在 s1 引用的对象上注册 bye 回调。
ender = weakref.finalize(s1, bye)
# 4. 调用 finalize 对象之前,.alive 属性的值为 True。
print(ender.alive) # True
# 5. del 不删除对象,而是删除对象的引用。
del s1
print(ender.alive) # True
# 6. 重新绑定最后一个引用 s2,让 {1, 2, 3} 无法获取。
# 对象被销毁了,调用了 bye 回调 ender.alive 的值变成了 False。
s2 = 'spam' # Gone with the wind...
print(ender.alive) # False
示例的目的是明确指出 del 不会删除对象,但是执行 del 操作后可能会导致对象不可获取,从而被删除。
你可能觉得奇怪,为什么示例中的 {1, 2, 3} 对象被销毁了?毕竟,我们把 s1 引用传给 finalize 函数了,而为了监控对象和调用回调,必须要有引用。这是因为,finalize 持有 {1, 2, 3} 的弱引用。
8.6 弱引用
正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后, 垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间(例如缓存技术),这就用到了弱引用。
弱引用不会增加对象的引用数量。引用的目标对象称为所指对象 (referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。
示例 8-17 弱引用是可调用的对象,返回的是被引用的对象;如果所指对象不存在了,返回 None;
⚠️注意:在控制台中,Python 会自动把 _ 变量绑定到结果不为 None 的表达式结果上,这种不明显的隐式赋值会为对象创建新引用。
>>> import weakref
>>> a_set = {0, 1}
# 1. 创建弱引用对象 wref
>>> wref = weakref.ref(a_set)
# 2. 调用 wref() 返回的是被引用的对象,{0, 1}。
# 因为这是控制台会话,所以 {0, 1} 会绑定给 _ 变量。
>>> wref()
{0, 1}
>>> _
{0, 1}
# 3. a_set 不再指代 {0, 1} 集合,因此集合的引用数量减少了。
# 但是 _ 变量仍然指代它,所以仍然可以获取原来引用的对象。
>>> a_set = {2, 3, 4}
>>> wref()
{0, 1}
>>> _
{0, 1}
# 4. 计算这个表达式时,{0, 1} 存在,因此 wref() 不是 None。
# 但是,随后 _ 绑定到结果值 False,现在 {0, 1} 没有强引用了。
>>> wref() is None
False
>>> _
False
# 5. 因为 {0, 1} 对象不存在了,所以弱引用指向的是 None,所以 wref() 返回的是 None。
>>> wref() is None
True
8.6.1 WeakValueDictionary简介:
WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从 WeakValueDictionary 中删除。因此,WeakValueDictionary 经常用于缓存。
示例:
import weakref
class Cheese:
def __init__(self, kind):
self.kind = kind
def __repr__(self):
return 'Cheese(%r)' % self.kind
# 1. 创建个弱引用类型的字典
stock = weakref.WeakValueDictionary()
# 2. 奶酪列表
catalog = [Cheese('Red Leicester'), Cheese('Tilsit'), Cheese('Brie'), Cheese('Parmesan')]
for cheese in catalog:
# 3. 将 stock 以奶酪的名为键,然后把值映射到 catalog 中 Cheese 的实例的弱引用上。
stock[cheese.kind] = cheese
# 以键名排序,现在 stock 是完整的
print(sorted(stock.keys())) # ['Red Leicester', 'Tilsit', 'Brie', 'Parmesan']
# 4. 删除奶酪列表,即删除 stock 中值的弱引用对象,所以值对应的键也会随即删除。
del catalog
print(sorted(stock.keys())) # ['Parmesan']
# 5. 显示删除全局变量 cheese 后,最后一个 Parmesan 消失。
del cheese
print(sorted(stock.keys())) # []
注意示例中的第 4 点,删除 catalog 后,列表中的大部分奶酪示例都被删除了,但是为什么还有一个 Parmesan 呢?这是因为临时变量 cheese 引用了对象,如果是在局部变量里(函数的内部变量),这么就不会有这个问题,因为在函数返回时局部变量都会被销毁。 但是这里的 cheese 是全局变量,除非显式的删除,否则不会消失,所以 cheese 最后一个引用的对象 Parmesan 引用计数没有归零,也不会被垃圾回收,所以 WeakValueDictionary 中的弱引用是仍然可以获取到的对象。再次显式的删除 cheese,最后一个 Parmesan 便会消失。
与 WeakValueDictionary 对应的是 WeakKeyDictionary,后者的键是弱引用。WeakKeyDictionary 实例可以为应用中其他部分拥有的对象 附加数据,这样就无需为对象添加属性。这对覆盖属性访问权限的对象尤其有用。
weakref 模块还提供了 WeakSet 类,按照文档的说明,这个类的作用很简单:“保存元素弱引用的集合类,元素没有强引用时,集合会把它删除”。 如果一个类需要知道所有实例,一种好的方案是创建一个 WeakSet 类型的类属性,保存实例的引用。如果使用常规的 set,实例永远不会被垃圾回收,因为类中有实例的强引用,而类存在的时间与 Python 进程一样长,除非显式删除类。
8.6.2 弱引用的局限:
不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本的 list 和 dict 实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题:
class MyList(list):
"""list的子类,实例可以作为弱引用的目标"""
# a_list 可以作为弱引用的目标
a_list = MyList(range(10))
wref_to_a_list = weakref.ref(a_list)
set 实例可以作为所指对象,但是 int 和 tuple 实例不能作为弱引用的目标,甚至它们的子类也不行。