Python 内置的 functools.lru_cache
是一个非常实用的装饰器,用于缓存函数调用的结果。当你多次调用一个输入参数相同的函数时,它会直接返回第一次调用时保存的结果,避免重复计算。
在这个 functools.lru_cache
里面,我们已经知道 Cache 是缓存的意思,那么 LRU 是什么意思呢?
LRU 是 “Least Recently Used”(最近最少使用)缓存淘汰策略。
当缓存满了,新数据进来时,就把最久没用过的缓存项淘汰。
这个策略在操作系统、浏览器缓存等领域也很常见,属于一种“聪明”的缓存替换方式。
那为什么 LRU 要跟 Cache 搭配使用呢?
当我们给函数加缓存时,本质上是“用空间换时间”——用内存记录住以前计算过的结果,省去重复计算。这在大多数情况下都是提升性能的好办法。
但是,缓存有一个潜在的问题:内存是有限的。如果你缓存的函数被调用了成千上万次,而且每次参数都不同,那缓存就可能无限增长,导致程序越来越慢,甚至崩溃(内存溢出)。
这时候,就需要一种策略来决定哪些缓存可以被清理掉。这就是 LRU 的用武之地。
使用方式:
from functools import lru_cache
@lru_cache(maxsize=None) # 无限缓存def slow_function(x): print(f"Computing {x}...") return x * x
那为什么这里用了 maxsize=None
?
@lru_cache(maxsize=None)
这里的 maxsize=None
表示:无限制缓存所有输入对应的结果。
None
的含义是 缓存所有调用结果,无上限,不会自动淘汰旧数据。
也就是说:
- 每次调用函数,如果参数不同,结果都会被永久保存到缓存中;
- 即使缓存内容再多,也不会删除任何旧记录;
- 等于是一个“无边界的字典”在记忆所有调用过的输入和输出。
举个直白的例子:
@lru_cache(maxsize=None)def compute(x): print(f"Computing {x}") return x * x
如果你调用:
for i in range(100000): compute(i)
- 这会在内存中缓存 100,000 个结果
- 哪怕后面你只再用到前面某几个值,后面都不会删除任何缓存
- 所以如果输入种类很多,可能爆内存
为什么这么设计?
这是给那些开发者使用“纯函数”的场景准备的:
- 函数输入不多(如递归、数学函数)
- 希望最大化缓存命中率
- 不怕内存占用(比如在受控脚本里临时跑)
那什么时候用 maxsize=None
是明智选择?
使用场景 | 适不适合用 None |
---|---|
递归 Fibonacci 数列(只会调用到 n 个值) | ✔️ 适合 |
枚举类型参数(比如 weekday/level) | ✔️ 适合 |
大批用户 ID、变动输入(如数据库查询、网络接口) | ❌ 不适合,会爆内存 |
常驻后台进程、Web 服务器中的函数缓存 | ❌ 不适合,应限制大小 |
maxsize=None
就是让缓存“记住一切”,绝不清理,非常适合函数调用参数种类少、但调用频繁的场景。
适用于这些场景:
- 你确定函数参数种类不多(比如整数范围小、枚举类型等)
- 你需要最大程度避免重复计算
- 程序运行时间有限,不担心内存长期堆积
优点:命中率最高(只要计算过就能命中)
缺点:可能造成内存膨胀,如果函数被调用的参数非常多(比如数以万计)
那 maxsize
还能设置成什么?
maxsize
是缓存的“容量”:最多保留多少个不同参数的计算结果。最常用的设置方式有:
设置 | 含义 | 适用场景 |
---|---|---|
maxsize=None | 无限缓存 | 参数少,或用于一次性短时任务 |
maxsize=128 | 默认值,缓存最近使用过的 128 个输入结果 | 平衡性能与内存消耗 |
maxsize=256 | 更大的缓存 | 适用于参数多但有限的递归计算 |
maxsize=1 | 只保留最近一次调用结果 | 非常省内存,但效果有限 |
小实验建议:
你可以加上如下代码,观察缓存的行为:
@lru_cache(maxsize=3)def square(n): print(f"Computing square({n})") return n * n
square(2)square(3)square(4)square(2) # 命中缓存square(5) # 会淘汰最久未用的缓存(可能是 3)
总结
maxsize
控制缓存的容量大小,决定性能 vs. 内存的权衡None
表示无限缓存,适合数据少或一次性任务- 更常见的实战值是
maxsize=128
或256
- 合理设置
maxsize
是高性能编程的关键一环!
“如果我们不加装饰器,那函数是不是也在重复计算、占用内存?那和
@lru_cache(maxsize=None)
有什么区别?”
我们来系统对比一下:
情况一:没有加 @lru_cache
def slow_function(x): print(f"Computing {x}...") return x * x
每次你调用 slow_function(3)
:
- 都会 重新计算一次
- 之前计算过的结果 不会记住
- 不会额外占用内存(除了临时变量)
**情况二:加了 @lru_cache(maxsize=None)
@lru_cache(maxsize=None)def slow_function(x): print(f"Computing {x}...") return x * x
- 第一次调用会执行并缓存结果
- 第二次调用相同参数时,直接返回缓存值,不重新计算
- 会将每个不同参数的结果永久缓存,存储在内存中
- 如果调用参数非常多,缓存也会无限增长,内存占用会越来越大
所以,区别可以总结为:
情况 | 是否缓存 | 是否重复计算 | 是否增加内存占用 |
---|---|---|---|
无装饰器 | ❌ 否 | ✅ 是 | ❌ 不缓存,不增加 |
@lru_cache(maxsize=None) | ✅ 是 | ❌ 否 | ✅ 会一直增长 |
换句话说:
不加装饰器:
- 性能差(每次都算)
- 内存友好(啥都不留)
加
@lru_cache(maxsize=None)
:
- 性能好(重复输入直接命中)
- 内存可能爆(无限缓存)
那什么时候该缓存,什么时候不该?
- 如果函数非常慢/复杂,又经常用相同参数 ➜ 加缓存
- 如果函数每次输入都不同(如
time.time()
) ➜ 不加缓存 - 如果函数是 IO-bound(数据库/网络),用缓存可能有副作用 ➜ 谨慎使用
为什么“不加装饰器”时,虽然重复计算了,但内存更友好?#
我们先明确几个前提概念:
1. 什么会占用内存?
内存主要被以下几类东西占用:
- 函数运行时的局部变量 / 参数
- 数据结构(列表、字典、字符串等)
- 持久驻留的数据(如:缓存、全局变量、闭包等)
- 系统底层解释器维护的对象(如帧栈、对象引用)
2. 不加缓存的函数,每次运行都怎样?
来看例子:
def square(x): return x * x
square(100)square(100)square(100)
这里:
- 每次调用
square(100)
,都会执行一次x * x
- 这个乘法的中间计算值(临时变量)只在调用期间存在
- 一旦函数执行完,中间变量就被销毁、释放内存
- 除非你手动保存结果,否则 Python 不会占用内存来“记住”这个结果
所以你每次都在重新计算,但计算完就忘记,不会留存历史记录 —— 内存友好!
3. 而加了 @lru_cache
会怎样?
@lru_cache(maxsize=None)def square(x): return x * x
这个时候:
- 第一次
square(100)
会计算,并且把(100, 10000)
存到缓存 - 第二次相同输入时直接返回 10000,不再计算
- 但是!缓存中的
(100, 10000)
会一直留在内存中 - 如果你调用
square(x)
的参数很多,比如从 0 到 10万,那么缓存中会保留 10万个结果,占用大量内存!
核心区别
对比项 | 不加缓存(无装饰器) | 加缓存(@lru_cache ) |
---|---|---|
每次调用是否计算 | 是(总是重新算) | 否(重复输入直接命中) |
结果是否保留 | ❌ 不保留,临时变量用完即丢 | ✅ 保留所有计算过的参数和结果 |
内存使用 | 少,只在函数运行那一瞬间占点临时空间 | 多,缓存不断积累,会一直驻留在内存中 |
适合场景 | 输入多变、不重复,或对内存非常敏感的场景 | 输入稳定/重复率高,希望提速的场景 |
结论一句话:
每次都算虽然慢,但干净;缓存虽然快,但占空间。
Python 默认“每次都算”正是因为这是最简单、最节省内存的方式。是否缓存,取决于你的性能 vs 内存的权衡。
这个也跟内存回收(GC)机制,或变量生命周期有关系,我们之后会继续展开。
做 Sequenzo 经常容易遇到的问题:算大矩阵时爆内存#
这是因为我们要存储整个矩阵数据本身,比如几千行几千列,这本身就会占用大量内存。
也就是说,即使没有 @lru_cache
(没有手动缓存任何结果), 只要你把矩阵存在变量里(如 NumPy 数组、列表列表等),这块内存就不会被释放,除非你手动删除或变量生命周期结束
那在这种情况下,加 @lru_cache
有用吗?
通常没有用,甚至更糟。 原因:
@lru_cache
是用于缓存函数的返回值- 如果你的函数每次都接收不同的矩阵输入(比如一行行处理),那这些返回值就会不断被缓存
- 然而这些矩阵本身就很大,结果是:
- 原本你只是临时用完扔掉,现在你还多保留了一份在缓存中!
- 造成内存压力进一步上升
在处理大矩阵时,正确做法应该是:
目标 | 推荐做法 |
---|---|
节省内存 | - 分批处理(batch) - 用生成器 yield - 删除中间变量 del |
不重复计算 | 手动缓存关键步骤的中间结果,比如写入磁盘或保存为 .npy 文件 |
重复子计算频繁且参数少 | 这时才考虑 @lru_cache ,但前提是返回值本身不能很大 |
举个例子对比
不适合缓存的场景:
@lru_cache(maxsize=32)def compute_matrix(n): return np.random.rand(n, n) # 返回一个大矩阵
- 每次都生成不同的矩阵,重复率不高
- 每个返回值本身非常大,占用很多内存
- ➜ 不适合加缓存!
适合缓存的场景:
@lru_cache(maxsize=32)def factorial(n): if n == 0: return 1 return n * factorial(n - 1)
- 输入有限
- 返回值小
- 重复调用频繁
- ➜ 非常适合加缓存!
遇到的程序瓶颈 | 是否适合 @lru_cache |
---|---|
重复计算多,结果小 | ✔️ 适合 |
数据体积大(如矩阵) | ❌ 不适合,加了反而更耗内存 |
算法结构清晰、重复子问题多 | ✔️ 适合,特别是递归计算场景 |
适合使用缓存(如 @lru_cache
)的场景多吗?#
挺多的。尤其在计算密集型、递归、数据处理、Web 等领域,缓存是非常常见且需求量大的优化手段。 但是否适合使用 @lru_cache
这个具体工具,要根据具体场景判断。
那什么时候值得缓存?(高频场景)
1. 递归算法、动态规划#
- 典型例子:Fibonacci 数列、斐波那契路径、背包问题等
@lru_cache
可以大幅减少重复子问题的计算
@lru_cachedef fib(n): if n < 2: return n return fib(n - 1) + fib(n - 2)
2. Web 接口请求#
- 假设你有一个返回天气/汇率/热门文章的函数:
- 用户每分钟都请求一次,但数据变化没那么快
- 缓存能大幅减轻后端压力
@lru_cache(maxsize=100)def get_exchange_rate(currency): return requests.get(...)
3. 数据库/磁盘 I/O 层的缓存#
- 假设你查询了一个文件或者数据库中相同的数据
- 读取成本高,重复率高
- 缓存读结果,避免重复 I/O
4. 机器学习模型推理函数(特别是参数少的情况)#
- 有时候模型输入不变,但推理非常慢
- 缓存模型输出结果,加速 API 调用
但是,什么时候机器学习模型推理函数适合使用缓存?
前提是两个条件满足:
- 输入种类有限 / 重复率高
- 推理成本高(大模型、远程 API)
场景一:相同输入反复请求,比如 Web 服务中用户调用同一张图片
你部署了一个图像分类模型 API:
@lru_cache(maxsize=128)def classify_image(img_path): img = load_image(img_path) return model.predict(img)
用户上传的是重复的图(比如系统推荐图、常用模板图),每次处理都耗时 1 秒,用缓存就能大幅加速。
场景二:文本生成 / 向量检索系统中,常有重复请求
你有一个向量查询函数,用于文本检索:
@lru_cache(maxsize=512)def embed(text): return model.encode(text)
如果用户频繁查询“Python 教程”、“天气预报”等热门关键词,缓存 embedding 向量能节省 GPU 资源。
场景三:交互式问答机器人中常见问题重复
比如你部署了一个 NLP 模型回答 FAQ:
@lru_cache(maxsize=256)def qa(question): return model.answer(question)
当用户不断问“你是谁?”、“怎么联系你?”这类固定句子时,缓存结果就非常有用。
场景四:低频调用模型但不能容忍延迟
你在后端系统中每隔 5 分钟调用一次大模型(比如代码补全、审查),输入几乎固定:
@lru_cache(maxsize=16)def analyze_code(template_name): code = load_template(template_name) return model.review(code)
推理慢,但输入很少,缓存可以“掩盖”模型加载和运行的延迟
哪些机器学习模型推理不适合缓存?
情况 | 说明 |
---|---|
输入变化极大(如视频流、传感器数据) | 每次内容都不同,缓存命中率极低 |
对实时性要求极高(如交易决策) | 缓存结果可能滞后,带来错误 |
结果非确定性(如 GPT、温度大) | 同样输入,不同结果,缓存反而不可靠(unless 强制 deterministic) |
总结一句话:
如果你模型的输入是“重复率高 + 推理慢”,那加缓存几乎一定值得,
@lru_cache
就是快速上手方案。
5. 数据分析函数#
- 比如一个函数对某个文件/字段统计最大值、均值
- 如果你多次调用统计函数,输入一样,就适合缓存结果
不适合缓存的场景也不少#
场景 | 缓存适合吗? |
---|---|
每次输入都不同(如时间戳、UUID) | ❌ 不适合 |
返回值非常大(如几百 MB 的矩阵、图片) | ❌ 不适合 |
内存特别敏感(如嵌入式设备、移动端) | ❌ 不适合 |
对数据实时性要求极高(比如股票、传感器) | ❌ 不适合 |
工程中的结论:
- 缓存机制本身需求大,是很多高性能系统的核心组件
@lru_cache
是最简单、最容易上手的一种缓存形式,适用于轻量级、本地函数缓存- 更高级需求会使用 Redis/Memcached 等跨服务缓存方案
总结一句话:
只要你的程序里有“重复调用、成本高”的函数,就值得考虑缓存。
而 @lru_cache
就是这种需求中的“快速武器”🔧。
💡 小任务:尝试修改缓存大小(如 @lru_cache(maxsize=128)
),看看是否有性能变化?