Python 3.8+ 推荐用 @functools.cached_property 替代手动缓存,它线程安全、自动管理 __dict__,但仅适用于只读、不依赖可变状态的实例属性;需动态更新或带参时应改用 lru_cache。

用 @functools.cached_property 替代手写属性缓存
Python 3.8+ 提供了开箱即用的 @cached_property,它专为类实例级别的、只读、一次性计算的属性设计。比起自己用字典存结果或手动检查 hasattr,它更安全、线程安全(CPython 中对单个实例的首次访问是原子的),且自动处理 __dict__ 的写入和删除逻辑。
常见错误是把它用在需要动态更新的属性上——@cached_property 一旦计算完成就不会再触发函数,哪怕依赖的其他属性变了;也不适合用在类方法或静态方法上,它只作用于实例绑定的属性。
- 必须定义在类中,不能用于普通函数或
@staticmethod/@classmethod - 装饰的方法不能接受除
self外的参数 - 如果属性可能被显式赋值(如
obj.attr = x),会覆盖缓存,后续访问不再触发计算——这是预期行为,不是 bug - 示例:
from functools import cached_property <p>class ExpensiveModel: def <strong>init</strong>(self, data): self.data = data</p><pre class="brush:php;toolbar:false;">@cached_property def processed(self): print("Running heavy computation...") # 只打印一次 return sum(x**2 for x in self.data)
需要支持参数或可重算时,用 functools.lru_cache 手动管理
当你要缓存的是一个可带参的方法(比如 def get_result(self, key: str)),或者希望支持「强制刷新」,@cached_property 就不适用了。这时应把计算逻辑抽成独立的、纯函数式的实例方法,并用 @lru_cache 装饰——注意:必须确保参数是可哈希的,且方法不修改实例状态。
关键陷阱在于:直接对绑定方法(bound method)使用 @lru_cache 会导致缓存失效,因为每次访问 obj.method 都产生新对象。正确做法是将缓存放在函数内部,或用 functools.partial + 实例绑定,但更稳妥的是把缓存逻辑移到实例属性上:
- 在
__init__中初始化一个缓存函数:self._compute_cached = lru_cache(maxsize=128)(self._compute_raw) -
_compute_raw必须是普通实例方法,且只依赖传入参数和self的不可变字段(如 ID、配置) - 避免缓存依赖
self.data这类可变对象——除非你确定它不会变,否则缓存会过期却无法感知 - 若需清空缓存,调用
self._compute_cached.cache_clear()
避免误用 @property + 手动缓存导致的竞态与内存泄漏
有人会在 @property 下手动做 if not hasattr(self, '_result'): 判断并赋值,这看似简单,但在多线程环境下有竞态风险:两个线程同时判断失败、同时执行计算、同时写入,不仅浪费资源,还可能导致中间状态污染。
更隐蔽的问题是内存泄漏:如果缓存的是大型对象(如 NumPy 数组、DataFrame),而实例长期存活但实际不再需要该结果,手动缓存不会自动释放。相比之下,@cached_property 至少语义清晰;而 lru_cache 支持 maxsize 和 typed 参数,能更好控制生命周期。
- 不要在
@property中做 I/O 或网络请求并缓存结果——这类操作应明确分离出「加载」和「获取」两步 - 避免在缓存逻辑里引用
self的可变容器(如list、dict)作为参数,哈希值会变,导致缓存键错乱 - 调试时可通过
obj.__dict__查看已缓存的属性名,确认是否按预期写入
缓存失效策略比缓存本身更难设计
没有银弹。Python 没有内置的「监听 self.x 变化后自动清缓存」机制。如果你的昂贵结果依赖多个字段,又要求强一致性,就得自己维护依赖关系图,或改用更显式的刷新模式:
- 提供
.invalidate_cache()方法,由调用方在修改上游数据后主动调用 - 在关键 setter(如
@x.setter)里触发对应缓存清理,但要注意循环调用风险 - 对低频变更场景,可加时间戳 + TTL 判断,但需额外字段和逻辑
- 最务实的做法:承认「缓存 ≠ 实时」,在文档里写明该属性的更新时机和前提条件
真正棘手的从来不是“怎么缓存”,而是“什么时候不该缓存”——比如输入数据来自用户实时上传、或结果用于金融风控决策,这时候宁可多算几次,也不要省掉一次校验。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











