@cached_property能避免重复计算,因为它首次访问时执行方法并缓存结果到实例__dict__中,后续直接字典查找;而@property每次访问都重新执行方法体。

为什么 @cached_property 能避免重复计算
它把「首次调用方法 → 执行逻辑 → 存进实例 __dict__」这三步打包成一次操作,后续再访问同名属性,直接从 __dict__ 里取值,完全跳过函数体。不像普通 @property 每次都执行方法体,也不像手写缓存逻辑要自己管键名、判空、赋值。
常见错误现象:@property 包裹一个耗时 200ms 的数据库查询或 JSON 解析,在循环里读 100 次该属性,实际就跑了 20 秒;换成 @cached_property 后,首次 200ms,后面 99 次都是纳秒级字典查找。
- 只对实例生效:每个对象独立缓存,互不影响
- 不支持 setter:一旦缓存写入,就不能通过属性赋值覆盖(这是设计使然,不是 bug)
- 无法清空单个缓存:得手动删
obj.__dict__['attr_name'],或整个重建实例
@cached_property 和 @lru_cache 的关键区别在哪
两者都缓存结果,但作用域和机制完全不同:
-
@lru_cache缓存在函数层级,靠参数生成 key,适用于纯函数(无状态、输入决定输出),比如fibonacci(n) -
@cached_property缓存在实例层级,key 固定为属性名,依赖self状态,比如self._raw_data经清洗后生成的self.cleaned_records - 误用
@lru_cache在实例方法上会出错:默认不带self参数进 key,所有实例共享同一份缓存,导致数据污染
性能影响:如果方法不依赖 self,用 @lru_cache 更省内存(全局一份);如果必须依赖实例状态,@cached_property 是唯一安全选择。
什么时候不该用 @cached_property
它不是万能加速器,滥用反而引入隐性问题:
- 属性值会随其他属性变化而失效(比如
self.data更新了,但@cached_property缓存的self.processed还是旧的)—— 它不会自动监听依赖项 - 对象生命周期很长,且缓存值占用大量内存(如缓存了一个 500MB 的 NumPy 数组),会导致内存无法释放
- 需要在多线程环境下安全读写:
@cached_property本身不加锁,首次计算可能被多个线程同时触发(CPython 中因 GIL 多数情况不暴露,但非绝对安全)
替代方案:手动控制缓存生命周期,比如加 _invalidate_processed() 方法,在 data setter 里主动删 self.__dict__.pop('processed', None)。
Python 3.8+ 以外怎么兼容
低版本没有内置 @cached_property,但没必要重写整个描述符逻辑:
- 直接用 Flask 或 Django 的实现(它们早于标准库提供,代码稳定且轻量)
- 最简 fallback:在
__init__里预计算并赋值,比如self._expensive = self._compute_expensive(),放弃惰性求值换确定性 - 避免用第三方包如
cachetools去模拟——它面向函数缓存,强行套到属性上会破坏封装,且增加不必要的 key 构造开销
真正容易被忽略的是:@cached_property 的缓存写入发生在第一次 __get__,而这个时机取决于你何时首次访问该属性。如果初始化时就强制触发(比如在 __init__ 末尾写 self.expensive_attr),等于放弃惰性,但能确保后续所有路径看到一致状态——这点常被忽视,却直接影响调试难度。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











