Python 弱值缓存:放进 WeakValueDictionary 的对象为什么会自动消失

10-01 4阅读

缓存不一定负责把对象留住

一个解析器希望复用已经生成的文档对象,又不想让缓存长期占住全部文档。把普通字典换成 WeakValueDictionary 后,有些键却很快消失。这并不是缓存随机丢数据,而是弱值字典只帮忙找到仍被别处使用的对象,不承担长期持有职责。

普通变量、列表元素和普通字典的值通常形成强引用。只要仍有有效的强引用路径,对象便有继续存在的理由。弱引用不会单凭自己阻止回收;弱值字典在对象被回收时移除对应条目。因此,决定缓存是否命中的还有调用方的生命周期。

让两名使用者依次释放对象

下面例子在 Python 3.12 验证,只创建普通类实例,不打开文件。load 每次先查看弱缓存,缺失时才创建对象。created 统计真实创建次数;显式调用 gc.collect 只是为了让演示观察回收结果,不是业务代码每次查缓存都该做的动作。

Python 弱值缓存:放进 WeakValueDictionary 的对象为什么会自动消失

AI概念示意图,非真实界面

import gc
import weakref

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

cache = weakref.WeakValueDictionary()
created = 0

def load(name):
    global created
    document = cache.get(name)
    if document is None:
        document = Document(name)
        cache[name] = document
        created += 1
    return document

first = load('guide')
second = load('guide')
print('shared:', first is second)
assert first is second and created == 1
del first
gc.collect()
print('after first release:', 'guide' in cache)
assert 'guide' in cache
del second
gc.collect()
print('cache size after both:', len(cache))
assert len(cache) == 0
third = load('guide')
print('created:', created)
assert created == 2

strong = {'guide': third}
del third
gc.collect()
print('ordinary dict keeps:', strong['guide'].name)
try:
    cache['list'] = []
except TypeError:
    print('plain list: TypeError')
else:
    raise AssertionError('plain list unexpectedly accepted')

一次读取也会暂时成为持有者

前两行结果是 shared: True 和 after first release: True。两个变量拿到同一个对象,删除第一个变量时,第二个仍然持有它。等第二个也被删除并完成回收,缓存长度才变成零,下一次 load 的 created 变为二。

load 内先把 cache.get 的结果存入局部变量,再检查是否为 None。取到对象后,这个局部变量本身就是强引用,足以支撑当前函数继续使用它。不要把“先判断键是否存在”和“稍后再次取值”当成一个不会变化的整体。

这里使用 is None,而不是简单判断真假,因为业务对象可能自定义真假值,一个有效对象也可能表现为假。缓存未命中是接口返回的特定状态,应按这个状态处理;对象的业务含义应留给调用方判断。

把弱缓存放在正确的位置

最后用普通字典作对照:删除本地变量之后,字典里的对象仍然存在。两种容器服务不同目标。普通缓存适合保证近期结果能复用,再通过容量或过期策略控制内存;弱缓存适合共享当前已经有人持有的昂贵对象。

并非所有类型都支持弱引用。例子中的普通类实例可以使用,直接放入内置 list 会触发 TypeError,代码也验收了这个边界。带有 __slots__ 的类若需要弱引用支持,还要在设计时考虑 __weakref__,不要等上线后才发现对象类型不兼容。

弱引用也不是按秒过期的策略。条目何时消失取决于强引用和回收机制,不能据此保证对象至少缓存五分钟,更不能用它保存尚未落盘的唯一结果。应用若必须保留对象到任务结束,应让任务上下文明确持有强引用。

示例中的查找再创建没有实现并发互斥。多个线程同时发现未命中,仍可能创建多个实例。如果唯一实例具有业务含义,需要围绕检查和创建增加同步;如果只是优化,重复创建是否可接受也应写进约定,不能由弱字典替你决定。

排查缓存命中率时,除了统计访问次数,还应查看对象究竟由谁持有。调试器、临时列表和记录对象的日志容器都可能意外延长生命周期。将持有关系画清,再选择强缓存或弱缓存,通常比不断调整垃圾回收频率更有效。

参考资料

  1. Python 官方文档:weakref 与 WeakValueDictionary

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。