← 返回题目列表

Python 的 functools.cached_property 是什么?和 property 有什么区别?

高频 中等 第 3 / 21 题 更新于 2026/07/31
Pythoncached_propertyproperty缓存

简化版

**functools.cached_property(Python 3.8+)是一个「带缓存的属性装饰器」——它像 property 一样让方法用「属性」的方式访问(obj.x 而不是 obj.x()),但第一次访问时会「计算一次并把结果缓存到实例上」,之后再访问直接返回缓存值、不再重新计算。**用途:把「计算开销大、但结果在对象生命周期内不变」的属性做成「惰性 + 缓存」——第一次用到才算(惰性)、算一次就存下来(缓存)。例子:@cached_property def expensive(self): return 复杂计算(...)——obj.expensive 第一次触发计算、之后直接返回。property 的区别:① property 每次访问都重新执行方法(没有缓存,适合「值会变、或很轻量」的属性);② cached_property 只算一次、缓存到 实例.__dict__(适合「计算贵、值不变」的属性)。实现原理:cached_property 是个「非数据描述符」——第一次访问触发计算后,把结果直接写进 实例.__dict__[属性名],由于实例字典优先级高于非数据描述符,之后访问直接命中字典(不再走描述符)。注意:① 缓存的值不会自动失效(对象状态变了缓存不更新);② 需要类有 __dict__(用了 __slots__ 且没留 __dict__ 就不能用)。核心记忆:cached_property 是惰性+缓存的 property(第一次算、之后返回缓存),适合计算贵且不变的属性;property 每次都算。

详细版

cached_property vs property

维度propertycached_property
每次访问重新执行方法只第一次执行、之后返回缓存
缓存缓存到 实例.__dict__
适合值会变/很轻量计算贵/值不变
setter支持不支持(只读+可覆盖)
描述符类型数据描述符非数据描述符
from functools import cached_property
import time

class DataSet:
    def __init__(self, data):
        self.data = data

    @cached_property
    def stats(self):                    # 计算贵、结果不变 → cached
        print("计算统计(只应看到一次)")
        time.sleep(1)                    # 模拟耗时
        return {"sum": sum(self.data), "avg": sum(self.data)/len(self.data)}

    @property
    def size(self):                     # 轻量/可能变 → 普通 property
        return len(self.data)

ds = DataSet([1, 2, 3, 4])
print(ds.stats)     # 打印"计算统计"+返回(第一次,触发计算)
print(ds.stats)     # 直接返回缓存(不打印,不重新计算)
print(ds.stats)     # 还是缓存

# 缓存存在实例字典里
print("stats" in ds.__dict__)   # True(第一次访问后,结果存进了 __dict__)

# 可以手动删除缓存(下次重新计算)
del ds.stats                    # 删除缓存
print(ds.stats)                 # 重新计算(因为缓存没了)

# 每个实例独立缓存
ds2 = DataSet([10, 20])
print(ds2.stats)                # ds2 有自己的缓存(和 ds 不共享)

# 对比:property 每次都执行
class Circle:
    def __init__(self, r): self.r = r
    @property
    def area(self):
        return 3.14 * self.r ** 2   # 每次访问都重算(r 可能变了)
c = Circle(5)
c.area          # 78.5
c.r = 10        # 半径变了
c.area          # 314(property 每次重算,反映最新 r)
# 若用 cached_property,改 r 后 area 还是旧值(缓存不失效)→ 不适合

# __slots__ 冲突(cached_property 需要 __dict__)
class WithSlots:
    __slots__ = ('data',)   # 没有 __dict__
    @cached_property
    def x(self): return 1
# WithSlots().x   # ✗ TypeError(没有 __dict__ 存缓存)

⚠️ 核心机制:cached_property 是一个「非数据描述符」——它只定义了 __get__(没有 __set__),第一次访问 obj.stats 时走描述符的 __get__、计算出结果后,直接把结果写进 obj.__dict__['stats'];由于「实例字典的优先级高于非数据描述符」,之后再访问 obj.stats 会先命中实例字典里的缓存值、根本不再触发描述符(这就是它「只算一次」的原理)。这和 property(数据描述符,有 __get____set__)的关键区别在于「优先级」:数据描述符优先级高于实例字典,所以 property 每次访问都必然走描述符的 __get__(重新执行)、无法被实例字典「短路」。选择:值「计算贵且在对象生命周期内不变」用 cached_property(惰性计算 + 缓存,如统计、解析结果、外部资源);值「会变、或计算很轻」用 property(每次反映最新状态,如依赖其他属性的派生值)。三个要注意的点:① 缓存不会自动失效——如果对象的状态改变了(self.data 变了),cached_property 的缓存还是旧值,需要手动 del obj.属性 清缓存;② 需要实例有 __dict__——用了 __slots__ 且没在 slots 里留 __dict__ 的类不能用 cached_property(没地方存缓存);③ Python 3.12 前有线程锁问题(旧版本 cached_property 有个全局锁、多线程访问不同实例会串行,3.12 移除了)。

完整版教学

一、cached_property 是什么

先理解它的定位:

functools.cached_property(Python 3.8+):带缓存的属性
  = property(属性访问语法)+ 缓存(只算一次)

行为:
  @cached_property
  def expensive(self):
      return 复杂计算()
  → obj.expensive 第一次访问:执行计算、缓存结果
  → 之后访问:直接返回缓存(不重新计算)

两个特性合一:
  ① 惰性(lazy):第一次访问才计算(不是创建对象就算)
  ② 缓存(cached):算一次存下来,之后复用

适用:"计算贵 + 结果不变"的属性
  ① 计算开销大(复杂统计、解析、聚合)
  ② 在对象生命周期内不变(或很少变)
  ③ 不一定每次都用到(惰性:用到才算)

对比手动实现缓存(老写法):
  class C:
      def __init__(self): self._cache = None
      @property
      def x(self):
          if self._cache is None:
              self._cache = compute()
          return self._cache
  → cached_property 一行搞定这个模式

访问语法:属性(不是方法)
  obj.expensive     # ✓ 像属性访问
  obj.expensive()   # ✗ 不是方法调用

所以 cached_property=property+缓存(惰性计算、算一次存下来),适合计算贵且不变的属性

functools.cached_property(Python 3.8+):带缓存的属性 = property(属性访问语法)+ 缓存(只算一次)。行为@cached_property def expensive(self): return 复杂计算()——第一次访问执行计算并缓存、之后访问直接返回缓存。两个特性合一① 惰性(lazy):第一次访问才计算(不是创建对象就算)、② 缓存(cached):算一次存下来复用适用:「计算贵 + 结果不变」的属性(计算开销大、生命周期内不变、不一定每次都用到)。对比手动实现缓存(老写法)if self._cache is None: self._cache = compute()——cached_property 一行搞定这个模式。访问语法:属性(不是方法)obj.expensive 对、obj.expensive() 错)。理解「cached_property=property+缓存(惰性计算第一次才算、缓存算一次存下来);适合计算贵+结果不变的属性;一行代替手动 if self._cache is None 缓存模式;访问是属性 obj.x 不是方法」,就理解了 cached_property。

二、和 property 的区别

理解两者的核心差异:

property:每次访问都执行方法
  @property
  def area(self):
      return 3.14 * self.r ** 2   # 每次 obj.area 都重算
  → 没有缓存,每次反映最新状态
  → 适合"值会变"或"计算轻量"的属性

cached_property:只算一次、缓存
  @cached_property
  def stats(self):
      return 复杂计算()            # 第一次算、之后返回缓存
  → 有缓存,值固定(不反映后续变化)
  → 适合"计算贵"且"值不变"的属性

关键区别对照:
  维度         property          cached_property
  执行次数     每次访问都执行     只第一次
  缓存         无                有(实例 __dict__)
  值变化       每次反映最新       固定(缓存不失效)
  性能         每次都算           算一次(后续 O(1))
  setter       支持(可写)      不支持(只读+可覆盖)
  描述符       数据描述符        非数据描述符

选择:
  值依赖其他属性、会变 → property(每次算、反映最新)
    如 area 依赖 r(r 会变)→ 用 property
  值计算贵、不变 → cached_property(算一次)
    如 数据集的统计(data 不变)→ cached_property

误用后果:
  ① 值会变的用 cached_property → 缓存旧值(bug)
  ② 计算贵不变的用 property → 每次重算(浪费)

所以 property 每次执行(值会变/轻量)、cached_property 只算一次(计算贵/值不变)

property:每次访问都执行方法@property def area(self): return 3.14 * self.r ** 2 每次 obj.area 都重算、没缓存反映最新状态、适合值会变或计算轻量)。cached_property:只算一次、缓存(第一次算之后返回缓存、有缓存值固定不反映后续变化、适合计算贵且值不变)。关键区别对照:执行次数(每次 vs 只第一次)、缓存(无 vs 有实例 dict)、值变化(每次反映最新 vs 固定缓存不失效)、性能(每次都算 vs 算一次后续 O(1))、setter(支持 vs 不支持)、描述符(数据描述符 vs 非数据描述符)。选择:值依赖其他属性会变用 property(area 依赖 r)、值计算贵不变用 cached_property(数据集统计 data 不变)。误用后果:值会变的用 cached_property 会缓存旧值(bug)、计算贵不变的用 property 每次重算(浪费)。理解「property 每次执行(值会变/轻量、反映最新)、cached_property 只算一次(计算贵/值不变、缓存固定);property 支持 setter、cached_property 只读;误用:值会变用 cached 缓存旧值 bug、计算贵不变用 property 浪费」,就掌握了两者区别。

三、实现原理——非数据描述符

理解 cached_property 为什么能「短路」:

描述符优先级(关键背景):
  访问 obj.x 时的查找优先级:
    数据描述符(有 __get__ + __set__/__delete__)
      > 实例字典 obj.__dict__
      > 非数据描述符(只有 __get__)/类属性

cached_property 是"非数据描述符"(只有 __get__):
  → 优先级低于实例字典

工作流程:
  ① 第一次 obj.stats:
     实例字典没有 stats → 走类里的 cached_property.__get__
     __get__ 执行方法、计算结果 result
     __get__ 把 result 写进 obj.__dict__['stats']  ← 关键!
     返回 result
  ② 之后 obj.stats:
     实例字典有 stats(优先级高于非数据描述符)
     → 直接返回 obj.__dict__['stats'](命中缓存)
     → 根本不再走 cached_property.__get__(不重新计算)

图示:
  第一次: obj.stats → __dict__ 无 → __get__ 计算 → 存入 __dict__
  之后:   obj.stats → __dict__ 有 → 直接返回(短路,不走 __get__)

为什么 property 不能这样(数据描述符):
  property 有 __set__(是数据描述符)
  → 优先级高于实例字典
  → 每次访问都走 property.__get__(无法被实例字典短路)
  → 所以 property 每次都执行

del obj.stats 清缓存:
  删掉 obj.__dict__['stats']
  → 下次访问实例字典没有 → 又走 __get__ 重新计算

所以 cached_property 是非数据描述符:第一次算完存进实例__dict__,之后实例字典优先命中(短路)

描述符优先级(关键背景):访问 obj.x 的查找优先级——数据描述符(有 __get__ + __set__/__delete__)> 实例字典 obj.__dict__ > 非数据描述符(只有 __get__)/类属性cached_property 是「非数据描述符」(只有 __get__ → 优先级低于实例字典。工作流程① 第一次 obj.stats:实例字典没有 → 走 cached_property.__get__、执行方法计算 result、把 result 写进 obj.__dict__['stats']、返回;② 之后 obj.stats:实例字典有 stats(优先级高于非数据描述符)→ 直接返回缓存、不再走 __get__为什么 property 不能这样(数据描述符):property 有 __set__(数据描述符、优先级高于实例字典)、每次访问都走 property.__get__(无法被实例字典短路)。del obj.stats 清缓存:删掉 obj.__dict__['stats']、下次访问又走 __get__ 重新计算。理解「cached_property 是非数据描述符(只有__get__、优先级低于实例字典);第一次__get__计算后存进 obj.dict、之后实例字典优先命中短路(不再走__get__);property 是数据描述符(有__set__、优先级高于实例字典)所以每次都走__get__;del 清缓存」,就掌握了实现原理。

四、注意事项与陷阱

理解使用的坑:

陷阱1:缓存不会自动失效
  @cached_property
  def total(self):
      return sum(self.items)
  obj.total          # 算一次、缓存
  obj.items.append(5)  # 数据变了
  obj.total          # 还是旧值!(缓存不更新)
  解决:
  ① 手动 del obj.total(下次重算)
  ② 数据会变的别用 cached_property(用 property)

陷阱2:需要实例有 __dict__
  class C:
      __slots__ = ('x',)   # 没有 __dict__
      @cached_property
      def y(self): ...
  C().y   # ✗ TypeError(没地方存缓存)
  解决:__slots__ 里加 '__dict__',或别用 cached_property

陷阱3:属性名和缓存键一致(描述符要知道自己的名字)
  cached_property 靠 __set_name__ 知道属性名(3.6+ 自动)
  → 一般不用管,但把同一个 cached_property 赋给两个名字会出问题

陷阱4:只读(不支持 setter)
  cached_property 不支持 @x.setter
  → 但因为是非数据描述符,可以直接 obj.x = v 覆盖缓存(实例字典赋值)
  obj.total = 100   # 直接设实例字典,覆盖缓存(合法但要小心)

陷阱5:Python < 3.12 的线程锁
  3.8~3.11 的 cached_property 有个"每属性的锁"
  → 多线程访问"不同实例的同一属性"会串行(性能)
  → 3.12 移除了这个锁

陷阱6:可变默认/共享
  缓存是"每实例独立"(存实例 __dict__),不共享 → 这点是好的

所以陷阱:缓存不自动失效(数据变要 del)、需要__dict__(__slots__冲突)、只读、旧版线程锁

使用的坑陷阱1:缓存不会自动失效obj.total 算一次缓存、obj.items.append(5) 数据变了、obj.total 还是旧值,解决手动 del obj.total 或数据会变的用 property);陷阱2:需要实例有 __dict__(用了 __slots__ 没留 __dict__ 会 TypeError、解决 slots 里加 '__dict__');陷阱3:属性名和缓存键一致(靠 __set_name__ 知道属性名、别把同一个赋给两个名字);陷阱4:只读(不支持 setter)(但因是非数据描述符、可直接 obj.x = v 覆盖缓存);陷阱5:Python < 3.12 的线程锁(3.8~3.11 有每属性的锁、多线程访问不同实例的同一属性会串行、3.12 移除);陷阱6:可变默认/共享(缓存每实例独立不共享、这点是好的)。理解「陷阱:①缓存不自动失效(数据变要 del、会变的用 property)②需要__dict__(__slots__冲突)③靠__set_name__知道名字④只读但可直接赋值覆盖⑤旧版(3.8-3.11)线程锁、3.12 移除;缓存每实例独立不共享」,就掌握了注意事项。

五、和其他缓存方案对比

理解 cached_property 和别的缓存方式:

几种缓存方案对比:

① cached_property:缓存"实例的属性"
   @cached_property
   def x(self): ...
   → 每个实例算一次、缓存到实例
   → 适合"实例级的、不变的、计算贵的属性"

② functools.lru_cache:缓存"函数的返回值"(按参数)
   @lru_cache
   def compute(n): ...
   → 按参数缓存(不同参数不同缓存)
   → 适合"纯函数、按参数缓存"
   ⚠️ 用在方法上会导致实例无法被回收(缓存持有 self)→ 内存泄漏

③ property + 手动缓存(老写法):
   @property
   def x(self):
       if self._x is None: self._x = compute()
       return self._x
   → cached_property 的手动版(3.8 前用)

④ functools.cache(3.9+):lru_cache(maxsize=None) 的简写

选择:
  实例的、算一次的属性 → cached_property
  函数按参数缓存 → lru_cache / cache
  方法结果按参数缓存 → 小心 lru_cache 的内存泄漏
    (用 cached_property 或 weakref 或 cachetools)

cached_property vs lru_cache 在方法上:
  cached_property:缓存存实例 __dict__,实例回收时缓存也回收(安全)
  lru_cache 在方法上:缓存字典持有 self 引用 → 实例无法回收(泄漏)
  → 方法级"无参数、算一次"的缓存 → 用 cached_property(更安全)

所以 cached_property 缓存实例属性(算一次、随实例回收);lru_cache 按参数缓存(方法上有泄漏风险)

几种缓存方案对比① cached_property:缓存「实例的属性」(每个实例算一次、缓存到实例、适合实例级的不变的计算贵的属性);functools.lru_cache:缓存「函数的返回值」(按参数)(按参数缓存、适合纯函数、用在方法上会导致实例无法被回收——内存泄漏);③ property + 手动缓存(老写法)(3.8 前用);functools.cache(3.9+)lru_cache(maxsize=None) 的简写)。选择:实例的算一次的属性用 cached_property、函数按参数缓存用 lru_cache/cache、方法结果按参数缓存小心 lru_cache 的内存泄漏。cached_property vs lru_cache 在方法上:cached_property 缓存存实例 dict、实例回收时缓存也回收(安全);lru_cache 在方法上缓存字典持有 self 引用、实例无法回收(泄漏)——方法级「无参数、算一次」的缓存用 cached_property(更安全)。理解「cached_property 缓存实例属性(算一次、随实例回收安全);lru_cache 按参数缓存函数(用在方法上持有 self 导致内存泄漏);方法级无参算一次用 cached_property、函数按参数用 lru_cache/cache」,就掌握了和其他缓存方案的对比。

六、总结与实践

总结 cached_property:

核心:
  cached_property = 惰性 + 缓存的属性(3.8+)
  第一次访问计算并缓存到实例 __dict__、之后返回缓存

vs property:
  property:每次访问都执行(值会变/轻量)
  cached_property:只算一次(计算贵/值不变)

实现原理(非数据描述符):
  第一次走 __get__ 计算、存进实例 __dict__
  之后实例字典优先命中(短路,不走描述符)
  property 是数据描述符(每次走 __get__)

适用:
  计算贵 + 结果在对象生命周期内不变的属性
  (统计、解析、聚合、外部资源)

注意事项:
  ① 缓存不自动失效(数据变要手动 del)
  ② 需要实例有 __dict__(__slots__ 冲突)
  ③ 只读(但可直接赋值覆盖缓存)
  ④ 3.12 前有线程锁

vs lru_cache(方法上):
  cached_property:缓存随实例回收(安全)
  lru_cache 在方法:持有 self → 内存泄漏

实践建议:
  ① 计算贵且不变的实例属性 → cached_property
  ② 值会变/轻量 → property
  ③ 数据变了记得 del 清缓存
  ④ 方法级无参缓存优先 cached_property(别用 lru_cache 防泄漏)

核心总结:
  cached_property 惰性+缓存(第一次算、之后返回缓存)
  非数据描述符(存实例 __dict__、后续短路)
  适合计算贵且不变;property 每次都算

所以 cached_property 惰性+缓存(第一次算、之后返回缓存),适合计算贵且不变;property 每次算

核心:cached_property = 惰性 + 缓存的属性(3.8+)、第一次访问计算并缓存到实例 __dict__、之后返回缓存。vs property:property 每次访问都执行(值会变/轻量)、cached_property 只算一次(计算贵/值不变)。实现原理(非数据描述符):第一次走 __get__ 计算存进实例 __dict__、之后实例字典优先命中短路、property 是数据描述符每次走 __get__适用:计算贵 + 结果在对象生命周期内不变的属性。注意事项:缓存不自动失效(数据变要 del)、需要实例有 __dict__、只读、3.12 前有线程锁。vs lru_cache(方法上):cached_property 缓存随实例回收(安全)、lru_cache 在方法持有 self 内存泄漏。理解「cached_property 惰性+缓存(第一次算、之后返回缓存);非数据描述符存实例__dict__后续短路;适合计算贵且不变、property 每次算;缓存不自动失效要 del、需要__dict__;方法级无参缓存用它别用 lru_cache 防泄漏」,就掌握了总结与实践。

记忆钩子:「functools.cached_property(Python 3.8+)是『带缓存的属性装饰器』——像 property 一样让方法用『属性』方式访问(obj.x 不是 obj.x()),但第一次访问时计算一次并把结果缓存到实例上,之后再访问直接返回缓存值、不再重新计算;适合把『计算开销大、但结果在对象生命周期内不变』的属性做成惰性+缓存;★和 property 的核心区别:property『每次访问都重新执行方法』(没缓存、适合值会变或很轻量的属性,如 area 依赖 r、r 变了要反映)、cached_property『只算一次、缓存到实例__dict__』(适合计算贵且值不变的属性,如数据集统计);★实现原理:cached_property 是『非数据描述符』(只有__get__、没有__set__),第一次访问走__get__计算、把结果写进 obj.dict[属性名],由于『实例字典优先级高于非数据描述符』,之后访问直接命中字典缓存、根本不再走描述符(这就是只算一次的原理);而 property 是『数据描述符』(有__set__、优先级高于实例字典)所以每次都走__get__执行;★注意:①缓存不会自动失效(对象状态变了缓存还是旧值,要手动 del obj.属性 清缓存,值会变的别用 cached_property)②需要实例有__dict__(用了__slots__且没留__dict__就不能用)③只读但可直接 obj.x=v 覆盖缓存④Python<3.12 有全局线程锁;vs lru_cache 用在方法上:cached_property 缓存随实例回收(安全)、lru_cache 持有 self 导致实例无法回收(内存泄漏)」

七、常见误区与追问

  • 误区:cached_property 和 property 一样,每次访问都执行方法。 不一样——property 每次访问都重新执行方法(没有缓存),而 cached_property 只在第一次访问时执行、把结果缓存到实例的 __dict__ 里,之后再访问直接返回缓存值、不再执行方法;所以 property 适合「值会变或计算很轻量」的属性(每次反映最新状态),cached_property 适合「计算开销大且值不变」的属性(算一次、后续 O(1))。
  • 误区:cached_property 的缓存会在对象状态改变时自动更新。 不会——cached_property 一旦缓存了值,就一直返回那个缓存值,即使你后来修改了它依赖的数据(self.items 变了),缓存也不会重新计算、还是返回旧值;这是它「不失效」的特性带来的坑;如果需要「重新计算」,要手动 del obj.属性 删除缓存(下次访问会重算);如果属性的值会随对象状态变化,就不该用 cached_property、应该用普通 property(每次都算、反映最新)。
  • 误区:任何类都能用 cached_property。 需要实例有 __dict__——因为 cached_property 把缓存值存在 实例.__dict__[属性名] 里,如果一个类用了 __slots__ 且没有在 slots 里包含 '__dict__',那这个类的实例就没有 __dict__、cached_property 无处存缓存、访问时会抛 TypeError;解决办法是在 __slots__ 里加上 '__dict__'(但这就抵消了 slots 省内存的目的)、或者不用 cached_property(改用 property + 手动缓存到某个 slot)。
  • 误区:cached_property 是数据描述符,和 property 一样。 不是——property 是「数据描述符」(同时有 __get____set__),cached_property 是「非数据描述符」(只有 __get__);这个区别决定了它们的行为:数据描述符的优先级高于实例字典,所以 property 每次访问都必然走它的 __get__(每次执行);非数据描述符的优先级低于实例字典,所以 cached_property 第一次算完把结果存进实例字典后,之后访问会优先命中实例字典的缓存、不再走描述符(只算一次);正是「非数据描述符 + 把结果写进实例字典」这个机制实现了缓存。
  • 追问:cached_property 是怎么实现「只计算一次」的? 靠「非数据描述符 + 实例字典优先级」:cached_property 是一个非数据描述符(只定义了 __get__、没有 __set__);第一次访问 obj.x 时,实例字典 obj.__dict__ 里还没有 x,于是查找会走到类里的 cached_property 描述符、调用它的 __get__——__get__ 执行你写的方法、算出结果、然后关键的一步是把这个结果直接写进 obj.__dict__['x'],再返回结果;之后再访问 obj.x 时,因为「实例字典的优先级高于非数据描述符」,Python 会先在 obj.__dict__ 里找到 x(就是上次缓存的值)、直接返回,根本不会再走到 cached_property 的 __get__(也就不会重新计算);这就是「第一次算、之后返回缓存」的原理;对比 property(数据描述符、优先级高于实例字典),它无法被实例字典「短路」、每次都走 __get__、所以每次都重新执行。
  • 追问:什么时候用 cached_property,什么时候用 property? 用 cached_property 的场景:属性的值「计算开销大」(复杂统计、解析大文件、聚合计算、访问外部资源)、且「在对象生命周期内不变」(依赖的数据不会改,或改了也不需要属性跟着变)、并且「不一定每次都用到」(惰性:用到才算,创建对象时不算)——这时缓存能省下重复计算的开销;用 property 的场景:属性的值「会随对象状态变化」(比如 area 依赖 r、r 会被修改、每次访问要反映最新的 r)、或者「计算很轻量」(就是几个简单运算、缓存的收益还不如缓存本身的复杂度)——这时每次重新计算是必要的(保证最新)或无所谓的(很快);误用会出问题:把「会变的值」用 cached_property 会返回过期的旧值(bug)、把「计算贵不变的值」用 property 会每次重算(浪费性能);此外 property 支持 setter(可写),cached_property 只读(虽然可以直接赋值覆盖缓存)。
  • 追问:cached_property 和 lru_cache 用在方法上有什么区别,为什么 lru_cache 在方法上有内存泄漏风险? 区别:cached_property 用于「实例的无参属性」——每个实例的这个属性算一次、缓存到该实例的 __dict__,缓存随实例走、实例被垃圾回收时缓存也一起回收;lru_cache 用于「按参数缓存函数返回值」——它维护一个全局的缓存字典、以参数为键存结果;把 lru_cache 用在实例方法上时,方法的第一个参数 self 也会成为缓存键的一部分、被缓存字典「持有引用」,这意味着只要缓存还在、这些实例就无法被垃圾回收(因为缓存字典一直引用着它们),造成内存泄漏(实例本该被回收却因为缓存的强引用而存活);所以「方法级、无参数、算一次」的缓存应该用 cached_property(缓存跟着实例走、安全),而不是 lru_cache;如果确实需要「方法按参数缓存」,要用能处理这个问题的方案(如给缓存加 weakref、用 cachetools 库、或接受泄漏风险并注意清理);functools.cache(3.9+)是 lru_cache(maxsize=None) 的简写、同样有这个方法上的泄漏问题。

八、加强记忆

functools.cached_property(Python 3.8+)是「带缓存的属性装饰器」——像 property 一样让方法用「属性」方式访问(obj.x 不是 obj.x()),但第一次访问时计算一次并把结果缓存到实例上、之后再访问直接返回缓存值、不再重新计算;适合把「计算开销大、但结果在对象生命周期内不变」的属性做成惰性 + 缓存。和 property 的核心区别:property「每次访问都重新执行方法」(没缓存、适合值会变或很轻量的属性,如 area 依赖 r)、cached_property「只算一次、缓存到实例 __dict__」(适合计算贵且值不变的属性,如数据集统计)。实现原理:cached_property 是「非数据描述符」(只有 __get__、没有 __set__),第一次访问走 __get__ 计算、把结果写进 obj.__dict__[属性名],由于「实例字典优先级高于非数据描述符」,之后访问直接命中字典缓存、根本不再走描述符(这就是只算一次的原理);而 property 是「数据描述符」(有 __set__、优先级高于实例字典)所以每次都走 __get__ 执行。注意:① 缓存不会自动失效(对象状态变了缓存还是旧值、要手动 del obj.属性 清缓存、值会变的别用)、② 需要实例有 __dict__(用了 __slots__ 且没留 __dict__ 就不能用)、③ 只读但可直接 obj.x = v 覆盖缓存、④ Python < 3.12 有全局线程锁。vs lru_cache 用在方法上:cached_property 缓存随实例回收(安全)、lru_cache 持有 self 导致实例无法回收(内存泄漏)。一句话「cached_property 惰性+缓存(第一次算、之后返回实例字典里的缓存),适合计算贵且不变的属性;是非数据描述符(算完存实例__dict__、后续实例字典优先短路),property 是数据描述符每次都执行;缓存不自动失效要 del、需要__dict__;方法级无参缓存用它别用 lru_cache(防内存泄漏)」。