Python 的 functools.lru_cache 有什么用?使用时要注意什么?
简化版
functools.lru_cache 是函数结果缓存装饰器,会根据函数参数缓存返回值,重复调用相同参数时直接返回缓存结果。它适合纯函数和计算昂贵的函数,但参数必须可哈希,并且要注意缓存大小、内存占用和数据过期问题。
详细版
示例:
from functools import lru_cache
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
没有缓存时,递归斐波那契会重复计算大量子问题;加上 lru_cache 后,相同 n 的结果只计算一次。
注意点:
- 函数参数必须是可哈希对象,比如整数、字符串、元组。
- 不适合缓存依赖外部状态、时间、数据库实时数据的函数,除非你能接受旧数据。
maxsize控制最多缓存多少组结果,None表示不限制大小。- 可以用
cache_info()查看命中情况,用cache_clear()清空缓存。
print(fib.cache_info())
fib.cache_clear()
面试回答要同时讲收益和边界:它能减少重复计算,但不是分布式缓存,也没有自动过期时间。
完整版教学
一、lru_cache 是什么
lru_cache 是标准库 functools 提供的装饰器。
from functools import lru_cache
@lru_cache(maxsize=128)
def query(key):
...
当你第一次调用:
query("a")
函数正常执行,并把参数 "a" 对应的结果存入缓存。下一次再调用 query("a"),如果缓存还在,就直接返回上次结果。
LRU 是 Least Recently Used,表示缓存满了以后,优先淘汰最近最少使用的条目。
二、为什么递归里很常用
看斐波那契:
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
计算 fib(5) 会重复计算 fib(3)、fib(2) 等子问题。
加缓存:
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
每个 n 只计算一次,复杂度会从指数级重复递归明显降低。
三、缓存键如何生成
lru_cache 会根据函数参数构造缓存键,所以参数必须可哈希。
可以:
@lru_cache
def f(x: int, name: str):
...
不可以:
@lru_cache
def f(items: list[int]):
...
列表不可哈希,会报 TypeError。
如果确实要缓存列表含义,可以把列表转成元组:
@lru_cache
def f(items: tuple[int, ...]):
...
f(tuple([1, 2, 3]))
四、哪些函数适合缓存
适合缓存的函数通常有这些特征:
- 相同输入总是得到相同输出;
- 计算成本较高;
- 参数空间不会无限膨胀;
- 结果可以在一段时间内复用;
- 没有写数据库、发消息这类副作用。
例如:
- 递归动态规划;
- 解析配置;
- 编译模板;
- 本地纯计算。
不适合直接缓存:
- 依赖当前时间的函数;
- 查询实时数据库的函数;
- 依赖用户权限变化的函数;
- 有副作用的函数。
五、maxsize 怎么选
@lru_cache(maxsize=128)
表示最多保留 128 组调用结果。缓存满了会淘汰最近最少使用的结果。
@lru_cache(maxsize=None)
表示不限制缓存大小。它可能带来内存增长风险,除非你确定参数种类有限。
工程里 maxsize 不应该随手写很大,要根据参数分布和内存预算设置。
六、缓存观测和清理
lru_cache 装饰后的函数有辅助方法:
fib.cache_info()
可以看到命中次数、未命中次数、最大容量、当前大小。
fib.cache_clear()
可以清空缓存。
在测试里,如果缓存影响用例隔离,记得清理缓存:
def test_fib():
fib.cache_clear()
assert fib(10) == 55
七、常见误区与追问
记忆钩子:
lru_cache缓的是“同样入参对应的返回值”。它适合纯函数和重复计算,不适合把变化的外部世界偷偷缓存起来。
- 误区:加了
lru_cache函数一定更快。 第一次调用仍要计算,缓存还会带来哈希、锁和内存开销;只有重复调用比例高、计算成本明显时才划算。 - 追问:为什么参数必须可哈希? 缓存需要用函数参数构造 key 放进字典,列表、字典这类可变不可哈希对象不能直接作为缓存键。
- 误区:缓存结果会自动随数据库或文件变化更新。
lru_cache不知道外部数据源是否变化,缓存失效要靠cache_clear()、版本参数或更明确的业务缓存策略。 - 追问:
maxsize=None有什么风险? 它表示不限制缓存数量,长生命周期服务中可能因参数种类不断增长而占用越来越多内存。 - 误区:实例方法加
lru_cache只按业务参数缓存。 实例方法的self也参与 key;如果大量实例调用同一方法,缓存可能按实例分裂,甚至延长实例生命周期。 - 追问:怎么观察缓存是否有效? 用
func.cache_info()查看 hits、misses、maxsize、currsize;如果 misses 很高,说明缓存命中率低,可能不值得保留。
八、加强记忆
lru_cache 是“按参数缓存函数结果”的装饰器,适合纯计算和重复子问题。它快的前提是参数可哈希、结果可复用、缓存大小可控。面试时要讲清它不是万能缓存:没有自动过期,不适合实时数据和副作用函数。