描述符通过__get__方法将计算结果缓存到instance.__dict__中实现延迟计算,避免重复执行昂贵逻辑。关键是要检查实例字典是否存在该属性,而非存在描述符自身;若需复用,应在__set_name__中动态绑定名称。

描述符怎么避免重复计算昂贵属性
延迟计算的核心是“第一次访问才算,之后直接返回缓存结果”,描述符通过 __get__ 方法控制这个过程。关键不是每次调用都重新执行逻辑,而是把结果存在实例字典里,下次直接取。
常见错误是把缓存值存在描述符自身(即类属性),导致所有实例共享结果;或者忘了检查实例字典中是否已存在该属性,反复计算。
- 缓存必须写入
instance.__dict__,不能写进描述符对象的属性里 - 务必先用
hasattr(instance, self.name)或直接查instance.__dict__.get(self.name)判断是否存在,再决定是否计算 - 如果属性名固定,可硬编码;若需复用描述符,建议在
__set_name__中动态绑定名称
LazyProperty 描述符的最小可用实现
一个真正能用的延迟描述符,要支持实例独享缓存、兼容属性赋值拦截、不破坏 help() 和 IDE 补全(靠 __doc__ 传递)。
class LazyProperty:
def __init__(self, func):
self.func = func
self.name = func.__name__
self.__doc__ = func.__doc__
<pre class="brush:php;toolbar:false;">def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
if self.name not in instance.__dict__:
instance.__dict__[self.name] = self.func(instance)
return instance.__dict__[self.name]
立即学习“Python免费学习笔记(深入)”;
注意:这里没实现 __set__,所以对实例赋值会覆盖缓存值——这是预期行为,符合 Python 属性语义;若想禁止赋值,需加空的 __set__ 并抛 AttributeError。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
为什么不用 @property + 手动缓存
直接在 @property 里查 self.__dict__ 看似简单,但会导致三个实际问题:
- 每个需要延迟计算的属性都要重复写缓存逻辑,容易漏掉
hasattr判断或写错键名 - 无法统一管理缓存策略(比如加锁、过期、日志),后期扩展成本高
- IDE 和类型检查工具(如 mypy)对
@property的返回类型推断更弱,而描述符可通过泛型或注解增强提示
例如,下面这段看似无害的代码其实每次都会调用 expensive_calc:
@property
def value(self):
# ❌ 错误:没检查缓存,每次都算
return self.expensive_calc()
和 functools.cached_property 的区别与取舍
Python 3.8+ 提供了 functools.cached_property,它本质就是个内置的延迟描述符。但要注意几个边界差异:
-
cached_property不允许实例手动赋值(写入会触发AttributeError),而自定义描述符可自由选择是否拦截 -
cached_property在多线程下是安全的(内部用了锁),自定义实现若没加锁,在并发首次访问时可能重复计算 -
cached_property不支持删除(del obj.attr会失败),自定义描述符可以加__delete__支持重算
如果你不需要定制行为,优先用 cached_property;如果要控制赋值、删除、线程策略或兼容老版本 Python,就自己写描述符。
最常被忽略的一点:延迟计算只对「读多写少」且「构造后基本不变」的属性有效。如果属性依赖外部状态(比如当前时间、配置文件内容),缓存会导致结果过期——这时候就得加失效逻辑,而不是单纯靠描述符。

















