lru_cache:别再手写记忆化字典了
用标准库 @lru_cache / @cache 给纯函数做记忆化;参数必须可哈希,别缓存副作用,也别原地改返回值。
要解决的问题
写 Python 后端时,经常有一类函数:同样的入参,结果一定一样,但算一次很贵——解析大配置、按键查只读映射、递归算组合数、按 PEP 号拉文档文本等。同一进程里同一参数被问很多次,若每次都重算,CPU 和 I/O 都白费。
标准库 functools 里的 @lru_cache(自 Python 3.2)就是干这件事的:用字典记住「参数 → 返回值」,下次命中直接返回。需要无上限缓存时,Python 3.9+ 还有更轻的 @cache(等价于 lru_cache(maxsize=None))。本文示例已在本机 Python 3.13.5 跑通;核心 API 在 3.9+ 一致。
核心概念
先记住这几条(以 Python 3.12 functools 文档 为准):
@lru_cache(maxsize=128, typed=False):装饰器,把函数包成带记忆的可调用对象,最多保留maxsize条最近调用。maxsize=None时关闭 LRU 淘汰,缓存可无限增长。- 参数必须可哈希:内部用字典存结果,位置参数和关键字参数都要 hashable。传
list/dict会TypeError: unhashable type: ...。 @cache(3.9+):无界缓存,比带上限的lru_cache更小更快;适合参数空间小、结果稳定的纯函数。cache_info()/cache_clear():前者返回CacheInfo(hits, misses, maxsize, currsize),方便调maxsize;后者清空缓存。3.9+ 还有只读的cache_parameters()。- 线程安全:文档写明缓存结构在并发更新时保持一致;但另一线程在首次调用尚未写完缓存前再调一次,仍可能执行多次底层函数。
- 适用边界:只适合「想复用已算过的值」的场景。有副作用、每次要新对象(如生成器)、或不纯的函数(如
time()/random())不该缓存。
示例代码
「贵」的纯函数 + 命中复用(本地已跑通):
from functools import lru_cache, cache
calls = {"n": 0}
@lru_cache(maxsize=32)
def load_config(env: str) -> dict:
calls["n"] += 1
# 这里假装一次很贵的只读加载
return {"env": env, "debug": env == "dev"}
a = load_config("prod")
b = load_config("prod") # 命中缓存,不再进函数体
c = load_config("dev")
assert calls["n"] == 2
assert a is b # 返回的是同一份缓存对象
print(load_config.cache_info())
# CacheInfo(hits=1, misses=2, maxsize=32, currsize=2)
load_config.cache_clear()
@lru_cache
def fib(n: int) -> int:
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
assert fib(10) == 55
assert fib.cache_info().hits > 0
@cache
def square(x: int) -> int:
return x * x
assert square(3) == 9
assert square.cache_info().maxsize is None
@lru_cache 也可不写括号(3.8+),默认 maxsize=128。需要按类型分开缓存时用 typed=True:例如 f(3) 与 f(3.0) 会各占一条。关键字参数顺序不同也可能记成两条,如 f(a=1, b=2) 与 f(b=2, a=1)(见官方文档说明)。
参数不可哈希时会立刻失败:
from functools import lru_cache
@lru_cache
def bad(xs: list) -> int:
return len(xs)
# bad([1, 2]) → TypeError: unhashable type: 'list'
# 可改成 tuple:bad((1, 2))
常见误区
-
缓存有副作用的函数
文档明确:不要缓存会改库表、打日志当业务、读「当前时间」的函数。命中时副作用不会再发生,行为会 silently 错。 -
改返回值却忘了它是缓存对象
lru_cache返回的是缓存里那一份引用。上面a is b为真;若a["debug"] = True,下次命中也会看到被改过的值。只读结果,或返回不可变结构 / 深拷贝后再改。 -
maxsize=None/@cache用在参数空间很大的服务上
无界缓存会一直涨。长跑的 Web 进程更稳妥的是设合理maxsize,并用cache_info()看命中率再调。 -
给方法加
@lru_cache却不理解self也进缓存键
实例本身会参与键;实例多、生命周期长时,等于按实例囤一份结果,还可能拖住实例不被回收。实例级昂贵属性更常见的做法是functools.cached_property(3.8+),细节见官方「How do I cache method calls?」。 -
把
list/dict当参数
不可哈希就进不了缓存。能稳定下来的键请改成tuple、frozenset或规范化后的字符串。
小结
同一入参会反复问、结果又稳定时,优先标准库 @lru_cache(要上限)或 @cache(3.9+、无上限):少写手账字典,还自带 cache_info / cache_clear。参数必须可哈希;别缓存副作用;别原地改返回值;长跑服务慎用无界缓存。正式项目以你当前解释器版本的 functools 文档 为准。
参考资料
谢谢你读到这里。