Python弱引用

Python弱引用

介绍

引用的存在使对象在内存中保持活力。当对象的引用计数为零时,垃圾回收器就会将其丢弃。但有时,为了避免对象在内存中停留过久,拥有该对象的引用是非常有用的。一个常见的用例就是缓存。

 

对对象的弱引用不会增加其引用计数。作为引用目标的对象称为引用对象。因此,我们说弱引用不会阻止引用对象被垃圾回收。

 

弱引用在缓存应用程序中非常有用,因为您不希望缓存的对象仅仅因为被缓存引用而保持活动状态。

 

示例 1 展示了如何调用 weakref.ref 实例来访问其引用对象。如果对象是存活的,调用弱引用会返回该对象,否则返回 None

 

Tip示例 1 是一个控制台会话,Python 控制台自动将变量绑定到非 None 表达式的结果。这干扰了我的预期演示,但也强调了一个实际问题:当尝试对内存进行微观管理时,我们常常会对隐藏的隐式赋值感到惊讶,这些赋值创建了对对象的新引用。控制台变量就是一个例子。回溯对象是意外引用的另一个常见来源。

 

示例 1. 弱引用是一个可调用对象,它返回被引用的对象,如果引用对象不再存在,则返回 None

>>> import weakref

>>> a_set = {0, 1}

>>> wref = weakref.ref(a_set)  (1)

>>> wref

<weakref at 0x100637598; to 'set' at 0x100636748>

>>> wref()  (2)

{0, 1}

>>> a_set = {2, 3, 4}  (3)

>>> wref()  (4)

{0, 1}

>>> wref() is None  (5)

False

>>> wref() is None  (6)

True

1wref 弱引用对象在下一行中创建并检查。

2)调用 wref() 返回引用的对象 {0, 1}。由于这是控制台会话,因此结果 {0, 1} 绑定到 _ 变量。

3a_set 不再引用 {0, 1} 集合,因此其引用计数减少,但 _ 变量仍然引用该集合。

4调用 wref() 仍返回 {0, 1}

5当计算此表达式时,{0, 1} 存在,因此 wref() 不是 None。但 _ 随后会绑定到结果值 False,所以现在不再有对 {0, 1} 的强引用。

6由于 {0, 1} 对象现已消失,因此对 wref() 的最后一次调用将返回 None

 

weakref 模块文档指出,weakref.ref 类实际上是一个用于高级用途的低级接口,并且大多数程序通过使用weakref 集合和finalize 就可以更好的满足开发需求。换句话说,请考虑使用 Wea​​kKeyDictionaryWeakValueDictionaryWeakSet finalize(在内部使用弱引用),而不是手动创建和处理您自己的weakref.ref 实例。我们只是在示例 1 中这样做,希望通过显示单个的weakref.ref 来消除它们周围的一些谜团。但在实践中,大多数时候Python程序都使用weakref集合。

 

下一小节将简要讨论weakref 集合。

 

WeakValueDictionary

WeakValueDictionary 类实现了可变映射,其中值是对对象的弱引用。当程序中其他地方对引用的对象进行垃圾收集时,相应的键会自动从 WeakValueDictionary 中删除。这通常用于缓存。

 

我们演示 WeakValueDictionary 的灵感来自于 Monty Python 的经典小品《奶酪店》,在这个小品中,一位顾客要求购买 40 多种奶酪,包括切达奶酪和马苏里拉奶酪,但都没有现货[1]

 

示例 2 实现了一个简单的类来表示每种奶酪。

2. 奶酪有种类属性和标准表示法

class Cheese:
    def __init__(self, kind):
        self.kind = kind
    def __repr__(self):
        return f'Cheese({self.kind!r})'

在例 3 中,每种奶酪都是从catelog中加载到使用 WeakValueDictionary 实现的库存(stock)中的。然而,一旦删除catelog,除了一种奶酪外,所有奶酪都会从库存(stock)中消失。你能解释为什么帕尔马干酪比其他奶酪持续的时间更长吗?

 

3. 顾客: "你们这里到底有没有奶酪?"

>>> import weakref
>>> stock = weakref.WeakValueDictionary()  (1)
>>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),
...                 Cheese('Brie'), Cheese('Parmesan')]
...
>>> for cheese in catalog:
...     stock[cheese.kind] = cheese  (2)
...
>>> sorted(stock.keys())
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit']  (3)
>>> del catalog
>>> sorted(stock.keys())
['Parmesan']  (4)
>>> del cheese
>>> sorted(stock.keys())
[]

1stock 是一个 WeakValueDictionary

2stock字典中将奶酪的名称映射到catelog中的奶酪实例的弱引用。

3stock齐全

4删除catelog后,大多数奶酪都会从stock中消失,正如 WeakValueDictionary 中所预期的那样。在这种情况下,为什么不是全部呢?

 

Tips临时变量可能会通过持有对对象的引用来导致对象的持续时间比预期更长。这通常不是局部变量的问题:它们在函数返回时被销毁。但在示例 3 中,for 循环变量 cheese 是一个全局变量,除非显式删除,否则永远不会消失。

 

WeakValueDictionary 相对应的是 WeakKeyDictionary,其中的键是弱引用。weakref.WeakKeyDictionary 文档提示了可能的用途:

 

[WeakKeyDictionary] 可用于将附加数据与应用程序其他部分拥有的对象相关联,而无需向这些对象添加属性。这对于覆盖属性访问的对象特别有用。

 

weakref 模块还提供了一个 WeakSet,在文档中简单地描述为保留对其元素的弱引用的 集合类。当不再存在对其的强引用时,该元素将被丢弃。如果您需要构建一个了解其每个实例的类,一个好的解决方案是创建一个带有 WeakSet 的类属性来保存对实例的引用。否则,如果使用常规集,实例将永远不会被垃圾收集,因为类本身对它们具有强引用,并且类与 Python 进程一样存在,除非您故意删除它们。

 

这些集合以及一般的弱引用在它们可以处理的对象类型方面受到限制。下一节将解释。

 

弱引用的局限性

并非每个 Python 对象都可以成为弱引用的目标或引用对象。基本的 list dict 实例可就不行,但这两者的普通子类可以轻松解决这个问题:

 

 

 

 

class MyList(list):
    """list subclass whose instances may be weakly referenced"""
 
a_list = MyList(range(10))
 
# a_list can be the target of a weak reference
wref_to_a_list = weakref.ref(a_list)

集合实例可以是指称对象,这就是示例 1 中使用集合的原因。用户定义的类型也不会造成问题,这解释了为什么示例 3 中需要愚蠢的 Cheese 类。但是 int tuple 实例不能作为弱引用的目标对象,即使创建了这些类型的子类。

 

这些限制大部分是 CPython 的实现细节,可能不适用于其他 Python 解释器。它们是内部优化的结果,其中一些优化将在下一节中讨论。

 


评论

此博客中的热门博文

OAuth 2教程

网格策略

apt-get详细使用