返回介绍

8.6 弱引用

发布于 2024-02-05 21:59:47 字数 4942 浏览 0 评论 0 收藏 0

正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。

弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。

弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象。

示例 8-17 展示了如何使用 weakref.ref 实例获取所指对象。如果对象存在,调用弱引用可以获取对象;否则返回 None。

 示例 8-17 是一个控制台会话,Python 控制台会自动把 _ 变量绑定到结果不为 None 的表达式结果上。这对我想演示的行为有影响,不过却凸显了一个实际问题:微观管理内存时,往往会得到意外的结果,因为不明显的隐式赋值会为对象创建新引用。控制台中的 _ 变量是一例。调用跟踪对象也常导致意料之外的引用。

示例 8-17 弱引用是可调用的对象,返回的是被引用的对象;如果所指对象不存在了,返回 None

>>> import weakref
>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set)  ➊
>>> wref
<weakref at 0x100637598; to 'set' at 0x100636748>
>>> wref()  ➋
{0, 1}
>>> a_set = {2, 3, 4}  ➌
>>> wref()  ➍
{0, 1}
>>> wref() is None  ➎
False
>>> wref() is None  ➏
True

❶ 创建弱引用对象 wref,下一行审查它。

❷ 调用 wref() 返回的是被引用的对象,{0, 1}。因为这是控制台会话,所以 {0, 1} 会绑定给 _ 变量。

❸ a_set 不再指代 {0, 1} 集合,因此集合的引用数量减少了。但是 _ 变量仍然指代它。

❹ 调用 wref() 依旧返回 {0, 1}。

❺ 计算这个表达式时,{0, 1} 存在,因此 wref() 不是 None。但是,随后 _ 绑定到结果值 False。现在 {0, 1} 没有强引用了。

❻ 因为 {0, 1} 对象不存在了,所以 wref() 返回 None。

weakref 模块的文档指出,weakref.ref 类其实是低层接口,供高级用途使用,多数程序最好使用 weakref 集合和 finalize。也就是说,应该使用 WeakKeyDictionary、WeakValueDictionary、WeakSet 和 finalize(在内部使用弱引用),不要自己动手创建并处理 weakref.ref 实例。我们在示例 8-17 中那么做是希望借助实际使用 weakref.ref 来褪去它的神秘色彩。但是实际上,多数时候 Python 程序都使用 weakref 集合。

下一节简要讨论 weakref 集合。

8.6.1 WeakValueDictionary简介

WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从 WeakValueDictionary 中删除。因此,WeakValueDictionary 经常用于缓存。

我们对 WeakValueDictionary 的演示受到来自英国六人喜剧团体 Monty Python 的经典短剧《奶酪店》的启发,在那出短剧里,客户问了 40 多种奶酪,包括切达干酪和马苏里拉奶酪,但是都没有货。3

3cheeseshop.python.org 还是 PyPI(Python Package Index 软件仓库)的别名,一开始里面什么也没有。写作本书时,Python Cheese Shop 中有 41 426 个包。还不错,但是与有 131 000 个模块的 CPAN(Comprehensive Perl Archive Network)相比,还差得远。所有动态语言社区都羡慕 CPAN 中有那么多模块。

示例 8-18 实现一个简单的类,表示各种奶酪。

示例 8-18 Cheese 有个 kind 属性和标准的字符串表示形式

class Cheese:

def __init__(self, kind):
  self.kind = kind

def __repr__(self):
  return 'Cheese(%r)' % self.kind

在示例 8-19 中,我们把 catalog 中的各种奶酪载入 WeakValueDictionary 实现的 stock 中。然而,删除 catalog 后,stock 中只剩下一种奶酪了。你知道为什么帕尔马干酪(Parmesan)比其他奶酪保存的时间长吗? 4 代码后面的提示中有答案。

4帕尔马干酪在工厂中至少要存储一年,因此它比其他新鲜奶酪的保存时间长。但是,这不是我们想要的答案。

示例 8-19 顾客:“你们店里到底有没有奶酪?”

>>> import weakref
>>> stock = weakref.WeakValueDictionary()  ➊
>>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),
...         Cheese('Brie'), Cheese('Parmesan')]
...
>>> for cheese in catalog:
...   stock[cheese.kind] = cheese  ➋
...
>>> sorted(stock.keys())
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']  ➌
>>> del catalog
>>> sorted(stock.keys())
['Parmesan']  ➍
>>> del cheese
>>> sorted(stock.keys())
[]

❶ stock 是 WeakValueDictionary 实例。

❷ stock 把奶酪的名称映射到 catalog 中 Cheese 实例的弱引用上。

❸ stock 是完整的。

❹ 删除 catalog 之后,stock 中的大多数奶酪都不见了,这是 WeakValueDictionary 的预期行为。为什么不是全部呢?

 临时变量引用了对象,这可能会导致该变量的存在时间比预期长。通常,这对局部变量来说不是问题,因为它们在函数返回时会被销毁。但是在示例 8-19 中,for 循环中的变量 cheese 是全局变量,除非显式删除,否则不会消失。

与 WeakValueDictionary 对应的是 WeakKeyDictionary,后者的键是弱引用。weakref.WeakKeyDictionary 的文档指出了一些可能的用途:

(WeakKeyDictionary 实例)可以为应用中其他部分拥有的对象附加数据,这样就无需为对象添加属性。这对覆盖属性访问权限的对象尤其有用。

weakref 模块还提供了 WeakSet 类,按照文档的说明,这个类的作用很简单:“保存元素弱引用的集合类。元素没有强引用时,集合会把它删除。”如果一个类需要知道所有实例,一种好的方案是创建一个 WeakSet 类型的类属性,保存实例的引用。如果使用常规的 set,实例永远不会被垃圾回收,因为类中有实例的强引用,而类存在的时间与 Python 进程一样长,除非显式删除类。

这些集合,以及一般的弱引用,能处理的对象类型有限。参见下一节的说明。

8.6.2 弱引用的局限

不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本的 list 和 dict 实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题:

class MyList(list):
  """list的子类,实例可以作为弱引用的目标"""

a_list = MyList(range(10))

# a_list可以作为弱引用的目标
wref_to_a_list = weakref.ref(a_list)

set 实例可以作为所指对象,因此实例 8-17 才使用 set 实例。用户定义的类型也没问题,这就解释了示例 8-19 中为什么使用那个简单的 Cheese 类。但是,int 和 tuple 实例不能作为弱引用的目标,甚至它们的子类也不行。

这些局限基本上是 CPython 的实现细节,在其他 Python 解释器中情况可能不一样。这些局限是内部优化导致的结果,下一节会以其中几个类型为例讨论(完全选读)。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文