Python标准库-强引用和弱引用

  |  

摘要: Python 标准库 weakref 模块,强引用和弱引用

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


1. weakref 总览

一个对象被引用可以分为强引用和弱引用,当对象的引用只剩弱引用时,有可能会被 GC 回收。但是,在实际销毁对象之前,即使没有强引用,弱引用也一直能返回该对象。

当对象被强引用时,对象的引用计数会自增,用于阻止该对象被 gc。这个行为有时候是不想要的,此时就可以考虑弱引用,也就是引用对象时引用计数并不自增,从而不影响被引用的对象被自动清理。

参考书(中英文版):

弱引用的使用场景

弱引用的一个主要用途是实现保存大型对象的缓存或映射,但又不希望大对象仅仅因为它出现在高速缓存或映射中而保持存活。大型对象如果不存在强引用,则不会保持活动状态(此时弱引用对象应该删掉)。

例如大型二进制图像对象,则可能希望将名称与每个对象关联起来。如果使用Python字典将名称映射到图像,则图像对象将保持活动状态,因为它们在字典中显示为值或键。weakref 模块提供的 WeakKeyDictionaryWeakValueDictionary 类可以替代Python字典,使用弱引用来构造映射,这些映射不会仅仅因为它们出现在映射对象中而使对象保持存活。如果一个图像对象是 WeakValueDictionary 中的值,那么当对该图像对象的剩余引用是弱映射对象所持有的弱引用时,垃圾回收可以回收该对象并将其在弱映射对象中相应的条目删除。

weakref 的主要组件

  • ref 返回弱引用对象,可以设置回调函数
  • finalize 提供了注册一个对象被垃圾收集时要调用的清理函数的方式。这比在原始弱引用上设置回调函数更简单,因为模块会自动确保对象被回收前终结器一直保持存活。
  • WeakKeyDictionary 和 WeakValueDictionary 在它们的实现中使用弱引用,在弱引用上设置回调函数,当键或值被垃圾回收回收时通知弱字典。

可以被弱引用的对象

可以被弱引用的对象包括类实例,用 Python(而不是用 C)编写的函数,实例方法、集合、冻结集合,某些文件对象,生成器,类型对象,套接字,数组,双端队列,正则表达式模式对象以及代码对象等。

内建类型 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 不支持弱引用,即使通过子类化也不支持。


2. 弱引用对象 weakref.ref

(1) 弱引用对象的特性

  • weakref.ref(obj) 返回对象 obj 的弱引用。如果 obj 仍然存活,则可以通过调用弱引用对象来访问到原始对象 obj。如果 obj 对象已经不存在,则调用弱引用对象返回 None。
  • 弱引用对象有回调函数,且弱引用对象依然存活,则对象 obj 即将终结时将调用回调函数弱引用对象将作为回调的唯一参数传递
  • 同一对象可以注册多个弱引用,回调函数调用顺序与注册顺序相反。
  • 回调所引发的异常将记录于标准错误输出,但无法被传播;它们会按与对象的 __del__() 方法所引发的异常相同的方式被处理。
  • 弱引用支持相等检测,但不支持排序比较。 如果被引用对象仍然存在,两个引用具有与它们的被引用对象一致的相等关系(无论 callback 是否相同)。
  • __callback__ 这个只读属性会返回当前关联到弱引用的回调。如果回调不存在或弱引用的被引用对象已不存在,则此属性的值为 None。弱引用对象没有 ref.callback 以外的方法和属性。

(2) 使用强引用和弱引用的对比

我们通过一个例子看看 del obj 时,对象存在强引用和只存在存在弱引用这两种情况下的行为。

注意 del 这个命令的特性:

  • del 删除的是名称,而不是对象。
  • del 命令并不会删除对象,而是当 del 删除了对象的最后一个引用时,会触发垃圾回收机制,回收器将对象销毁。

使用弱引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import weakref

class ExpensiveObject:
def __del__(self):
print("(Deleting {})".format(self))

obj = ExpensiveObject()
r = weakref.ref(obj)

print("obj: {}".format(obj))
print("ref: {}".format(r))
print("r(): {}".format(r()))

print("deleting obj")
del obj

print("ref: {}".format(r))
print("r(): {}".format(r()))

r 是弱引用对象
r() 是弱引用对象所引用的对象本身
当 del obj 后,r 的状态为 dead,已经无法访问原对象。

1
2
3
4
5
6
7
obj: <__main__.ExpensiveObject object at 0x7febba0d6eb8>
ref: <weakref at 0x7febbb685e58; to 'ExpensiveObject' at 0x7febba0d6eb8>
r(): <__main__.ExpensiveObject object at 0x7febba0d6eb8>
deleting obj
(Deleting <__main__.ExpensiveObject object at 0x7febba0d6eb8>)
ref: <weakref at 0x7febbb685e58; dead>
r(): None

使用强引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ExpensiveObject:
def __del__(self):
print("(Deleting {})".format(self))

obj = ExpensiveObject()
r = obj

print("obj: {}".format(obj))
print("ref: {}".format(r))

print("deleting obj")
del obj

print("ref: {}".format(r))

当 del obj 后,r 仍可继续访问原对象。

1
2
3
4
5
obj: <__main__.ExpensiveObject object at 0x7f02bd4fb240>
ref: <__main__.ExpensiveObject object at 0x7f02bd4fb240>
deleting obj
ref: <__main__.ExpensiveObject object at 0x7f02bd4fb240>
(Deleting <__main__.ExpensiveObject object at 0x7f02bd4fb240>)

(3) 使用回调函数

weakref.ref 在构造时,可以接受回调函数。该函数在引用的对象被删除时被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import weakref

class ExpensiveObject:
def __del__(self):
print("(Deleting {})".format(self))

def callback(reference):
print("callback({!r})".format(reference))

obj = ExpensiveObject()
r = weakref.ref(obj, callback)

print("obj: {}".format(obj))
print("ref: {}".format(r))
print("r(): {}".format(r()))

print("deleting obj")
del obj
print("ref: {}".format(r))
print("r(): {}".format(r()))

打印结果

1
2
3
4
5
6
7
8
obj: <__main__.ExpensiveObject object at 0x7fabc83a2240>
ref: <weakref at 0x7fabc8399e58; to 'ExpensiveObject' at 0x7fabc83a2240>
r(): <__main__.ExpensiveObject object at 0x7fabc83a2240>
deleting obj
(Deleting <__main__.ExpensiveObject object at 0x7fabc83a2240>)
callback(<weakref at 0x7fabc8399e58; dead>)
ref: <weakref at 0x7fabc8399e58; dead>
r(): None

从打印结果可以看到:回调函数接收引用时,该引用的状态已经 dead 了。因此该引用已经无法引用到原对象(调用该弱引用对象返回None)。

回调函数的主要作用是原对象即将终结时将弱引用对象(可能是集合的元素、字典的键值)从缓存中删掉


3. 终结器对象 weakref.finalize

使用终结器的主要好处在于它能更简便地注册回调函数,而无须保留所返回的终结器对象

终结器也可以被直接调用。但是终结器最多只能对回调函数发起一次调用。

(1) 终结器特性

1
weakref.finalize(obj, func, *args, **kwargs)
  • 返回一个可调用的终结器(finalizer)对象,该对象将在 obj 作为垃圾回收时被调用(也可以显式调用)。
  • 和普通的弱引用不同,终结器一直存活直到引用对象被回收
  • 终结器在调用之前是存活状态的,而调用(显式调用或在垃圾回收时隐式调用)后是死亡状态的。
  • 调用一个存活的的终结器返回func(*args, **kwargs)的结果,而调用 dead 状态的终结器返回 None.
  • 在垃圾收集期间由终结器回调所引发异常将显示于标准错误输出,但无法被传播。 它们会按与对象的 __del__() 方法或弱引用的回调所引发异常相同的方式被处理。
  • 当程序退出时,剩余的存活终结器会被调用,除非它们的 atexit 属性已被设为 False。它们会按与创建时相反的顺序被调用。
  • finalize 在构造时候传入的参数
    • 所关联的对象
    • 当所关联的对象被删除时回调的函数
    • 回调函数的 positional/named arguments
  • 注意要确保 func、args 和 kwargs 不拥有对 obj 的任何直接或间接引用,否则 obj 将永远不会被垃圾收集。func 也不应该是 obj 的一个绑定方法。

(2) 使用终结器例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import weakref

class ExpensiveObject:
def __del__(self):
print("(Deleting {})".format(self))

def on_finalize(*args):
print("on_finalize({!r})".format(args))

obj = ExpensiveObject()
f = weakref.finalize(obj, on_finalize, "extra argument")
f.atexit = False

del obj
1
2
(Deleting <__main__.ExpensiveObject object at 0x7fc87453e320>)
on_finalize(('extra argument',))

(3) 回调函数对 obj 有直接或间接引用的后果的例子

注意要确保 func、args 和 kwargs 不拥有对 obj 的任何直接或间接引用,否则 obj 将永远不会被垃圾收集。func 也不应该是 obj 的一个绑定方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import gc
import weakref

class ExpensiveObject:
def __del__(self):
print('(Deleting {})'.format(self))

def on_finalize(*args):
print('on_finalize({!r})'.format(args))

obj = ExpensiveObject()
obj_id = id(obj)

f = weakref.finalize(obj, on_finalize, obj)
f.atexit = False

# the finalize object holds a reference to obj
del obj

for o in gc.get_objects():
if id(o) == obj_id:
print('found uncollected object in gc')

从打印结果可以看到,del obj 并没有使得对象被回收。因为 finalizer 还对 obj 有强引用。

1
found uncollected object in gc

(4) 比较终结器与 __del__() 方法

假设我们想创建一个类,用它的实例来代表临时目录。 当以下事件中的某一个发生时,这个目录应当与其内容一起被删除:

  • 对象被作为垃圾回收,
  • 对象的 remove() 方法被调用
  • 程序退出。

使用 __del__() 方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class TempDir:
def __init__(self):
self.name = tempfile.mkdtemp()

def remove(self):
if self.name is not None:
shutil.rmtree(self.name)
self.name = None

@property
def removed(self):
return self.name is None

def __del__(self):
self.remove()

__del__() 方法不会阻止循环引用被作为垃圾回收。然而,__del__() 方法的处理会严重地受到具体实现的影响,因为它依赖于解释器垃圾回收实现方式的内部细节。

更健壮的替代方式可以是定义一个终结器,只引用它所需要的特定函数和对象,而不是获取对整个对象状态的访问权

使用终结器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import tempfile
import shutil
import weakref

class TempDir:
def __init__(self):
self.name = tempfile.mkdtemp()
self._finalizer = weakref.finalize(self, shutil.rmtree, self.name)

def remove(self):
self._finalizer()

@property
def removed(self):
return not self._finalizer.alive

t = TempDir()
t.remove()
print(t.removed)

像这样定义后,我们的终结器将只接受一个对其完成正确清理目录任务所需细节的引用。 如果对象一直未被作为垃圾回收,终结器仍会在退出时被调用。

(5) 当一个模块被卸载时运行特定代码

基于弱引用的终结器还具有另一项优势,就是它们可被用来为定义由第三方控制的类注册终结器,例如当一个模块被卸载时运行特定代码:

1
2
3
4
import weakref, sys
def unloading_module():
# implicit reference to the module globals from the function body
weakref.finalize(sys.modules[__name__], unloading_module)

4. 使用弱引用的代理

proxy 只是 ref 的一个更简洁的接口,跟 ref 的区别也只是使用上的区别。

有时用 proxy 比弱引用更方便。

  • 在使用 weak.ref 时,返回值 r,需要执行 r() 才是弱引用的对象
  • 而 weakref.proxy 的返回值直接就是弱引用的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import weakref

class ExpensiveObject:
def __init__(self, name):
self.name = name

def __del__(self):
print('(Deleting {})'.format(self))

obj = ExpensiveObject('My Object')
r = weakref.ref(obj)
p = weakref.proxy(obj)

print('via obj:', obj.name)
print('via ref:', r().name)
print('via proxy:', p.name)

del obj

print('via proxy:', p.name)

如果引用的对象 obj 被删除以后在访问代理,会报 ReferenceError 异常。

1
2
3
4
5
6
7
8
via obj: My Object
via ref: My Object
via proxy: My Object
(Deleting <__main__.ExpensiveObject object at 0x7f3dd2b9ce80>)
Traceback (most recent call last):
File "test.py", line 20, in <module>
print('via proxy:', p.name)
ReferenceError: weakly-referenced object no longer exists

5. WeakKeyDictionary 和 WeakValueDictionary

ref 和 proxy 是比较底层的方法,主要用于少量的个体对象的弱引用。

当对象比较多的时候,用 WeakKeyDictionary 和 WeakValueDictionary 是更合适的。

(1) WeakKeyDictionary

弱引用键的映射类。 当不再存在对键的强引用时,字典中的条目将被丢弃。这可被用来将额外数据关联到一个应用中其他部分所拥有的对象而无需在那些对象中添加属性。 这对于重载了属性访问的对象来说特别有用。

WeakKeyDictionary 的 keyrefs() 返回包含对键的弱引用的可迭代对象。这些弱引用不保证在它们被使用时仍然保持“存活”,因此这些引用的调用结果需要在使用前进行检测。

(2) WeakValueDictionary

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

WeakValueDictionary 的 valuerefs() 返回包含对键的弱引用的可迭代对象。这些弱引用不保证在它们被使用时仍然保持“存活”,因此这些引用的调用结果需要在使用前进行检测。

(3) 例子

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
47
48
49
50
51
import gc
import weakref
from pprint import pprint

gc.set_debug(gc.DEBUG_UNCOLLECTABLE)

class ExpensiveObject:
def __init__(self, name):
self.name = name

def __repr__(self):
return "ExpensiveObject({})".format(self.name)

def __del__(self):
print(" (Deleting {})".format(self))

def demo(cache_factory):
# Hold objects so any weak references are not removed immediately.
all_refs = {}

# Create the cache using the factory.
print('CACHE TYPE: {}'.format(cache_factory))
cache = cache_factory()

for name in ['one', 'two', 'three']:
o = ExpensiveObject(name)
cache[name] = o
all_refs[name] = o
del o

print(" all_refs =", end=" ")
pprint(all_refs)
print("\n Before, cache contains: {}".format(list(cache.keys())))
for name, value in cache.items():
print(" {} = {}".format(name, value))
del value

# Remove all references to the objects except the cache.
print("\n Cleanup:")
del all_refs
gc.collect()

print("\n After, cache contains:", list(cache.keys()))
for name, value in cache.items():
print(" {} = {}".format(name, value))
print(" demo returning")
return

demo(dict)
print("------")
demo(weakref.WeakValueDictionary)
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
CACHE TYPE: <class 'dict'>
all_refs = {'one': ExpensiveObject(one),
'three': ExpensiveObject(three),
'two': ExpensiveObject(two)}

Before, cache contains: ['one', 'two', 'three']
one = ExpensiveObject(one)
two = ExpensiveObject(two)
three = ExpensiveObject(three)

Cleanup:

After, cache contains: ['one', 'two', 'three']
one = ExpensiveObject(one)
two = ExpensiveObject(two)
three = ExpensiveObject(three)
demo returning
(Deleting ExpensiveObject(one))
(Deleting ExpensiveObject(two))
(Deleting ExpensiveObject(three))
------
CACHE TYPE: <class 'weakref.WeakValueDictionary'>
all_refs = {'one': ExpensiveObject(one),
'three': ExpensiveObject(three),
'two': ExpensiveObject(two)}

Before, cache contains: ['one', 'two', 'three']
one = ExpensiveObject(one)
two = ExpensiveObject(two)
three = ExpensiveObject(three)

Cleanup:
(Deleting ExpensiveObject(one))
(Deleting ExpensiveObject(two))
(Deleting ExpensiveObject(three))

After, cache contains: []
demo returning

Share