Python 的 lru_cache:缓存虽好,可变参数与内存这两处要先想清楚
Python 的 lru_cache:缓存虽好,可变参数与内存这两处要先想清楚
算斐波那契数列时,很多人第一次接触 functools.lru_cache 都会有一种“开了挂”的感觉:加一个装饰器,重复计算瞬间消失。但把它随手贴到业务函数上之前,有两个坑值得先想清楚:参数必须可哈希,以及缓存是有上限和生命周期的。
先看它解决什么问题
递归版斐波那契是经典例子,不加缓存时 fib(35) 要重复算海量子问题:
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=128 的意思是最多保留 128 条记录,满了就按“最近最少使用”(LRU)淘汰最久没用的那条。maxsize=None 则是不设上限——听着爽快,风险也在这里。
第一个坑:参数必须可哈希
缓存的键来自函数参数,参数必须可哈希。传列表、字典、集合会直接报错:
@lru_cache(maxsize=128)
def total(nums):
return sum(nums)
total([1, 2, 3]) # TypeError: unhashable type: 'list'
办法有两个:调用方传元组,或者在函数内部先把可变参数转成不可变形式再交给带缓存的内层函数。不要为了绕过报错而把参数转成字符串拼接——不同参数拼出同一个字符串时,缓存会返回错误结果。
第二个坑:缓存不会自己“过期”
lru_cache 只认参数,不认时间,也不认底层数据变没变。典型的翻车场景是:函数读数据库或读配置文件,加了缓存后数据改了,函数却一直返回旧结果。缓存适合“同样输入、结果稳定”的纯计算,比如解析、格式化、数学计算;不适合“数据随时会变”的查询。
需要手动清理时,装饰器自带两个工具:fib.cache_info() 看命中率和当前条数,fib.cache_clear() 清空缓存。调试缓存问题先看 cache_info(),命中率长期为 0 往往说明参数每次都在变,缓存根本没起作用。
三条使用建议
第一,只给纯函数加缓存,函数内部不要读外部可变状态。第二,maxsize 按数据规模给个数,不要图省事写 None,避免缓存把内存越撑越大。第三,参数里出现列表或字典时,先在外层转成元组或冻结结构,再进缓存函数。
用对地方,lru_cache 是一行代码换来的实实在在的加速;用错地方,它会在你最意想不到的时候返回旧数据。区分这两者的标准只有一个:这个函数的输出,是不是只由它的参数决定。
- 点赞
- 收藏
- 关注作者
评论(0)