Python 的 functools.cached_property 是什么?和 property 有什么区别?
简化版
**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:
| 维度 | property | cached_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(防内存泄漏)」。