本文介绍使用 python 3.8+ 的 `functools.cached_property` 装饰器,将耗时的类方法转为惰性求值、仅执行一次且自动缓存的属性,避免重复计算,兼顾性能与代码可读性。
在面向对象编程中,常遇到一类典型场景:某个计算逻辑开销大(如加载模型、解析大型配置、查询数据库),但结果在整个实例生命周期内恒定不变;同时该结果需被多个方法反复使用。若将其放在 __init__ 中,可能违反“延迟初始化”原则(例如依赖尚未就绪的外部资源);若每次调用都重新计算,则造成严重性能浪费。
Python 提供了简洁而地道的解决方案——functools.cached_property。它专为此类场景设计:将一个无参方法转换为
线程安全、只计算一次、结果自动缓存于实例字典中的只读属性
。首次访问时执行原方法并缓存返回值;后续访问直接返回缓存值,完全绕过函数调用开销。
以下是一个优化后的完整示例:
使用效果演示:
✅
关键优势
:
零侵入性
:无需修改调用方代码(self.my_value 语法保持不变,无需加括号);
类型友好
:类型提示仍可准确标注返回类型(如 -> int),IDE 和静态检查工具完全支持;
内存隔离
:缓存值存储在实例 __dict__ 中,不同实例互不干扰;
线程安全
:内部使用锁保障多线程首次访问的原子性(CPython 下默认安全);
符合 Python 哲学
:显式、简洁、可读性强,比手动实现 if not hasattr(self, '_my_value'): ... 更优雅。
⚠️
注意事项
:
cached_property 仅适用于
无参数、无副作用、结果确定性
的方法(即相同输入/状态必得相同输出);
缓存不可清除(除非手动 del obj.my_value),若需重算,请考虑 @property + 显式缓存控制,或改用 functools.lru_cache()(需确保方法可哈希);
Python < 3.8 用户可用 backports.cached-property 兼容包,或自行实现简易版(不推荐生产环境);
不适用于需要序列化(如 pickle)的场景——因缓存值不参与序列化,反序列化后会重新计算。
总结而言,@cached_property 是 Python 中解决“一次性惰性计算 + 多次复用”问题的标准答案。它让代码既高效又清晰,在数据处理、机器学习封装、配置中心客户端等高频场景中极具实用价值。
from functools import cached_property
from typing import List
class Example:
def __init__(self, data_source: str = "default") -> None:
self.data_source = data_source # 可用于构造时的轻量初始化
@cached_property
def my_value(self) -> int:
"""模拟耗时计算:仅在首次访问 self.my_value 时执行一次"""
print("→ 执行昂贵计算(仅一次)...")
# 此处可替换为真实耗时操作,如:
# - 加载预训练模型
# - 解析 GB 级 YAML 配置
# - 查询远程 API 并缓存响应
return sum(i * i for i in range(100_000)) # 模拟 CPU 密集型计算
def do_something_with_my_value(self) -> List[int]:
"""多次使用 my_value,但实际仅触发一次计算"""
return [x + self.my_value for x in range(1, 5)]
def another_method(self) -> str:
return f"基于 my_value({self.my_value}) 的衍生处理"ex = Example()
print(ex.do_something_with_my_value()) # 输出前打印 "→ 执行昂贵计算(仅一次)..."
print(ex.another_method()) # 不再打印,直接使用缓存值
print(ex.my_value) # 直接访问属性,仍为缓存值