用 functools.lru_cache 快速给函数做缓存,大幅提升重复计算的性能,用法超级简单。
lru_cache 是Python标准库自带的最近最少使用缓存装饰器,可以自动把函数的调用结果缓存下来,下次传入相同参数时直接返回缓存结果,不用重复计算。
python
from functools import lru_cache
@lru_cache(maxsize=None) # maxsize=None表示不限制缓存大小
def fib(n):
# 递归计算斐波那契数列,不加缓存的话n=40都会慢到卡
if n < 2:
return n
return fib(n-1) + fib(n-2)
for i in range(1,51):
print(i,":",fib(i)) # 瞬间出结果,不加缓存可能要算几分钟
常用高阶技巧
限制缓存大小:maxsize=128 会自动清理最久没用到的旧缓存,避免内存无限增长,适合线上生产环境。
清空缓存:调用 fib.cache_clear() 就能清空所有缓存,需要重新计算时直接调用即可。
查看缓存信息:fib.cache_info() 会输出命中次数、缺失次数、缓存大小,方便你调优。
适用场景
递归计算(如动态规划、斐波那契、树形遍历)
重复调用的昂贵计算(如读文件、数据库查询、接口请求)
参数固定、结果不会变的纯函数
注意:只有函数的参数是可哈希类型(整数、字符串、元组等不可变对象)才能用,如果参数是列表、字典这种可变对象,需要先转成可哈希类型再用。
实现原理
Python 中 functools.lru_cache 的实现原理主要基于哈希表(Hash Table)与双向链表(Doubly Linked List)的结合,这种数据结构组合通常被称为哈希链表。
其核心目标是在 𝑂(1)
O(1) 的时间复杂度内完成查找、插入和删除(淘汰)操作。以下是其底层实现的详细拆解:
- 核心数据结构
lru_cache 内部维护了两个关键结构:
字典(Dict):用于快速查找。
Key:由函数参数生成的哈希值(如果是 typed=False,则直接由参数元组构成;如果 typed=True,还会包含参数类型信息)。
Value:指向双向链表中节点的引用。
双向链表(Doubly Linked List):用于维护访问顺序。
链表头部(Head)代表最近最常使用(Most Recently Used, MRU)的数据。
链表尾部(Tail)代表最近最少使用(Least Recently Used, LRU)的数据,即下一次需要淘汰的对象。
- 工作流程
A. 缓存命中(Cache Hit)
当调用被装饰的函数时:
生成键将传入的参数打包成元组,并计算哈希值。
查字典:在字典中查找该键。
如果存在:
通过字典找到对应的链表节点。
移动节点:将该节点从当前位置移除,并移动到链表的头部(表示它刚刚被访问过,变为"最新")。
返回节点中存储的计算结果。
时间复杂度:𝑂(1)
O(1)
B. 缓存缺失(Cache Miss)
如果字典中找不到该键:
执行函数:调用原始函数获取结果。
检查容量:
如果缓存未满:创建一个新节点,存入结果,将其添加到链表**头部,并在字典中注册该键指向新节点。
如果缓存已满:
淘汰旧数据:移除链表尾部的节点(LRU 数据)。
清理字典:从字典中删除尾部节点对应的键。
插入新数据:创建新节点,存入结果,添加到链表头部,并在字典中注册。
时间复杂度:𝑂(1)
O(1)
- 关键实现细节
为什么用双向链表而不是数组?
移动效率高:在 LRU 策略中,每次访问数据都需要将其移动到"最新"位置。双向链表只需修改前后节点的指针即可在 𝑂(1)
O(1) 时间内完成移动。如果使用数组,移动元素需要搬运大量数据,复杂度为
𝑂(𝑁)
O(N)。
线程安全问题
非线程安全:标准的 functools.lru_cache 不是线程安全的。
原因:字典和链表的操作(如移动节点)不是原子性的。在多线程环境下,多个线程同时修改链表结构可能导致数据竞争或死锁。
解决方案:如果在多线程环境中使用,需要手动加锁(threading.Lock),或者使用第三方库如 cachetools 中的线程安全实现。
maxsize=None 的情况
如果设置 maxsize=None,则禁用 LRU 淘汰机制。
此时内部只使用一个字典,不再维护双向链表。性能会略微提升(少了链表操作开销),但内存会随着调用次数无限增长,直到程序崩溃或手动清除。
typed=True 的作用
默认情况下,f(3) 和 f(3.0) 被视为相同的键(因为 3 == 3.0 且 hash(3) == hash(3.0))。
如果设置 typed=True,键会包含类型信息,例如 (3, int) 和 (3.0, float) 被视为不同的键,分别缓存。这增加了缓存的精确度,但也增加了内存占用。