Python cached_property 缓存失效:输入已经改了,属性为什么仍然返回旧结果
一个报价对象把总量做成 cached_property,第一次读取很快得到结果。后来往原始列表追加一项,再读总量却没有变化。装饰器保存的是第一次计算的结果,并没有为输入列表安装变化监听。
下面用 calls 记录计算次数,分别观察重复读取、改变输入、删除缓存和直接赋值。保存为 demo.py,运行 python demo.py;只使用标准库与内存中的两个整数。
AI模型生成概念示意:对象保留第一次计算的结果卡,输入变化后需明确清除缓存;不是软件截图或实测界面。
from functools import cached_property
class Basket:
def __init__(self):
self.units = [2, 3]
self.calls = 0
@cached_property
def total(self):
self.calls += 1
return sum(self.units)
basket = Basket()
assert "total" not in vars(basket)
print("first:", basket.total, basket.calls)
assert basket.total == 5 and basket.calls == 1
assert vars(basket)["total"] == 5
basket.units.append(7)
print("after input change:", basket.total, basket.calls)
assert basket.total == 5 and basket.calls == 1
del basket.total
print("after deletion:", basket.total, basket.calls)
assert basket.total == 12 and basket.calls == 2
basket.total = 99
print("manual value:", basket.total, basket.calls)
assert basket.total == 99 and basket.calls == 2
del basket.total
print("recomputed:", basket.total, basket.calls)
assert basket.total == 12 and basket.calls == 3结果保存到实例的同名属性里
第一行是 first: 5 1:求和发生一次,值五写进实例的 total 属性。紧接着的断言再次读取 total,calls 仍为一。此后读取命中普通实例属性,不再进入原来的计算函数。查看 vars 可以直接确认缓存所在的位置。
第二行 after input change: 5 1 表明追加七并未触发重算。输入和缓存之间没有自动依赖关系,缓存也没有默认有效期。排查此类问题应同时检查原始数据与实例属性,不能因为函数体写着 sum,就认定每次点号访问都执行了求和。
删除是失效,赋值是覆盖
删除 total 后,下一次读取打印 after deletion: 12 2。删除动作本身没有求和;是随后的读取发现实例缺少同名属性,才再次调用函数。只删除某一实例的缓存,不会清空其他对象的结果。
manual value: 99 2 展示另一条允许的路径:直接赋值会覆盖保存的值,不要求定义 setter,也不触发计算。再删除后读取,得到 recomputed: 12 3。这与常见的只读 property 不同,接口设计不能默认外部无法改写它。
若数据确实会变,可以把修改入口集中到一个方法,并在修改后明确删除已存在的缓存。删除尚未生成的属性会报 AttributeError,所以失效方法还需处理首次读取前的情况。若任何地方都能直接改内部列表,集中失效很难保证完整;计算便宜时,普通 property 往往更容易维护。
cached_property 适合生命周期内基本不变、计算相对昂贵的实例资料。它需要可写的实例 __dict__。Python 3.12 起也不再提供旧版内部锁,多线程首次读取可能重复计算;需要“只能执行一次”的动作,应另行同步,不能把缓存装饰器当成执行次数保证。
资料核对日期:2026年10月2日。代码在本地 Python 3.12.14 实际运行并通过断言;结果对应文中固定输入。


