cached_property 更安全因其首次访问计算并存入实例 dict__,绕过描述符协议和 __getattr 陷阱;手动实现易致递归或线程不安全。

为什么 cached_property 比手动写 __getattr__ 更安全
因为 cached_property 在首次访问时计算值并直接注入实例的 __dict__,后续访问走的是普通属性查找路径,完全绕过描述符协议和 __getattr__ 的陷阱。而手写延迟逻辑容易在多线程下出错,或意外触发递归(比如在 __getattr__ 里又访问了同名属性)。
常见错误现象:RecursionError: maximum recursion depth exceeded,往往是因为属性 getter 里又读取了自身,没做初始化判断。
- 它只适用于 **实例级别** 的缓存,不共享于类或其它实例
- 一旦设值(
obj.attr = x),该属性就变成普通实例属性,cached_property不再生效 - 不支持删除(
del obj.attr会失败),除非你重写__delete__
什么时候不该用 cached_property
当属性值依赖外部状态(如文件内容、数据库记录、当前时间)且可能变更时,缓存会导致结果陈旧。例如:
@cached_property
def now(self):
return datetime.now() # ❌ 第一次调用后永远不变
使用场景受限还体现在:它不能接收参数,也不支持传入 self 以外的变量;若需条件加载(如根据 flag 决定是否查 DB),应改用普通方法或带参数的缓存方案(如 functools.lru_cache + 方法封装)。
立即学习“Python免费学习笔记(深入)”;
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
- 属性值是可变对象(如 list/dict)且外部可能修改它 → 缓存的是引用,行为符合预期,但要注意别误改
- 初始化开销极大但极少被访问 → 可能浪费资源,此时手动延迟加载 + 显式 flag 控制更清晰
- 需要支持
del或动态失效 → 得自己实现描述符类
cached_property 和 property + 手动缓存的区别
property 每次访问都执行函数;手动缓存(如在 __init__ 预算或用 if not hasattr(self, '_x'))则破坏了“按需”原则——可能初始化了根本用不到的属性。
cached_property 把这两者折中:真正第一次访问才计算,且自动处理线程安全(CPython 中通过 setattr 的原子性保证,无需额外锁)。
- Python 3.8+ 才有
cached_property;低版本可用backports.cached-property或自定义描述符 - 它不兼容 pickle(序列化时会丢失缓存值),反序列化后首次访问仍会重新计算
- 若属性方法抛异常,异常会被缓存 —— 下次访问直接 raise,不是重新计算
如何调试 cached_property 是否生效
最直接的方式是打日志或加断点看方法体是否仅执行一次:
from functools import cached_property
<p>class Expensive:
@cached_property
def data(self):
print("computing...")
return [i for i in range(1000000)]</p><p>e = Expensive()
print(e.data) # 触发 print
print(e.data) # 不再 print
另一个办法是检查实例字典:e.__dict__ 里会出现 'data' 键,值就是缓存结果;而纯 @property 不会出现在 __dict__ 中。
容易被忽略的地方:如果你在类定义之外给实例动态赋值同名属性(e.data = 'hack'),那之后就再也访问不到 cached_property 的逻辑了——它已经被实例属性覆盖,且不会自动清理。

















