Python 中 functools.lru_cache 缓存函数结果
什么是 functools.lru_cache
lru_cache 是 Python 标准库 functools 模块中的一个装饰器。它的全称是 Least Recently Used (LRU) cache,即“最近最少使用”缓存。其核心功能是:将一个函数的输入参数和对应的计算结果保存起来,当再次遇到相同的输入时,直接返回缓存中的结果,而不重复执行函数体。
当缓存容量达到上限时,它会自动丢弃那些最久没有被使用过的条目,为新的数据腾出空间。非常适合加速那些计算开销大、会被以相同参数反复调用的函数。
快速上手:一个最简单的缓存示例
先看一个没有使用缓存的递归斐波那契数列函数,随着参数增大,计算时间会急剧增加:
import time
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
start = time.time()
print(fib(35)) # 输出: 9227465
print(f"耗时: {time.time() - start:.2f} 秒")
在没有缓存的情况下,fib(35) 大约需要几秒钟(取决于机器性能)。接下来我们引入 lru_cache:
from functools import lru_cache
@lru_cache
def fib_cached(n):
if n < 2:
return n
return fib_cached(n-1) + fib_cached(n-2)
start = time.time()
print(fib_cached(35)) # 输出: 9227465
print(f"耗时: {time.time() - start:.6f} 秒") # 通常小于 0.001 秒
只需要在函数定义上方添加 @lru_cache,无需修改函数内部逻辑,性能就能得到质的飞跃。
lru_cache 的核心参数
lru_cache 提供了两个可选参数,用来控制缓存的行为:
maxsize:缓存的最大容量,即最多存储多少个不同的调用结果。默认值为128。- 当缓存条目数超过
maxsize时,最少使用的条目会被清除。 - 如果设置为
None,缓存可以无限制增长(不推荐,可能导致内存泄漏)。
- 当缓存条目数超过
typed:是否将不同类型的参数视为不同的调用,默认值为False。- 当
typed=False时,f(3)和f(3.0)会命中同一个缓存,因为3 == 3.0为True。 - 当
typed=True时,f(3)和f(3.0)会被分别缓存,视为两次不同的调用。
- 当
示例:指定 maxsize
@lru_cache(maxsize=256)
def compute(x, y):
# 模拟耗时计算
return x ** y + y ** x
示例:启用类型敏感缓存
@lru_cache(typed=True)
def double(value):
print(f"正在计算 {value}")
return value * 2
print(double(1)) # 计算一次,输出 2
print(double(1.0)) # 类型不同,重新计算,输出 2.0
print(double(1)) # 命中缓存,直接输出 2
查看缓存信息与手动管理
装饰后的函数会获得一个 cache_info() 方法,可以查看缓存的命中统计信息:
@lru_cache(maxsize=10)
def add(a, b):
return a + b
add(1, 2)
add(1, 2) # 命中缓存
add(3, 4)
info = add.cache_info()
print(info)
# 输出类似:CacheInfo(hits=1, misses=2, maxsize=10, currsize=2)
hits:缓存命中次数misses:未命中次数(即实际执行函数的次数)maxsize:缓存容量上限currsize:当前缓存的条目数量
另外,可以调用 cache_clear() 手动清空缓存:
add.cache_clear()
print(add.cache_info().currsize) # 输出 0
适用场景与最佳实践
适合使用 lru_cache 的场景
- 纯函数:函数的输出完全由输入决定,没有副作用(不依赖外部状态,不修改全局变量)。
- 计算密集型函数:递归、数学计算、字符串解析等。
- I/O 密集但结果可复用的函数:如读取固定内容的文件、请求同一个 API 端点(但需要注意数据新鲜度问题)。
- 动态规划等重复子问题的算法:如上面的斐波那契、背包问题、编辑距离等。
避免使用的情况
- 函数参数中包含不可哈希的类型(如列表、字典),因为缓存键必须是可哈希的。需要先将参数转换为可哈希类型(如元组)再传入。
- 函数有副作用(如写入文件、修改全局状态),使用缓存可能导致副作用被跳过。
- 返回结果的时效性非常强,缓存容易造成脏数据。
一个处理不可哈希参数的技巧
如果函数必须接收列表,可以定义一个包装函数,将列表转为元组:
@lru_cache
def sum_list(immutable_tuple):
return sum(immutable_tuple)
def calculate_sum(lst):
return sum_list(tuple(lst))
内部原理简述
lru_cache 底层依赖于一个有序字典(Python 3.7+ 中字典本身就保持插入顺序)外加一个双向循环链表(由 C 语言实现的 _lru_cache_wrapper 高效维护)。每次调用函数时:
- 根据传入参数计算出哈希键。
- 查询字典中是否存在该键。
- 若存在(命中),直接返回对应值,并将该条目移动到链表的头部(表示最近使用)。
- 若不存在(未命中),执行原函数,得到结果;如果缓存已满,则淘汰链表尾部的条目(即最久未使用),然后将新结果加入字典并移到头部。
由于 Python 字典的查找非常快,缓存带来的开销极小。
注意事项与潜在陷阱
-
参数顺序与默认值
f(1, 2)和f(y=2, x=1)最终生成的调用键相同,都会命中同一个缓存。 -
缓存穿透与无限增长
合理设置maxsize,避免使用None导致缓存无限膨胀,尤其是在处理大量不同参数组合时。 -
多线程环境下的安全性
lru_cache内部使用了一把锁来保证线程安全,但高并发时锁竞争可能成为瓶颈。对于高性能要求的场景,可以考虑使用functools.cache(Python 3.9+,它是一个简单的无界缓存,不限制大小但稍快)或第三方缓存方案。 -
Python 版本差异
- Python 3.2 引入
lru_cache。 - Python 3.9 新增
functools.cache,等价于lru_cache(maxsize=None),但性能更好。 - Python 3.8 开始,
lru_cache可以直接用于不带括号的装饰器(即@lru_cache等同于@lru_cache()),更早的版本需要写成@lru_cache()。
- Python 3.2 引入
总结
functools.lru_cache 是一个简单却强大的性能优化工具,通过记忆化函数调用结果,避免了大量重复计算。使用时牢记为纯函数、合理设置 maxsize,并注意参数的可哈希性。当你发现程序中有重复开销的函数时,不妨试试加上这个装饰器,往往能立即看到显著的加速效果。