Skip to content

Python 缓存与 CLI 工具入门(二):`@lru_cache` 实践

· 19 min

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)

为什么这么设计?

这是给那些开发者使用“纯函数”的场景准备的:

那什么时候用 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)

总结

“如果我们不加装饰器,那函数是不是也在重复计算、占用内存?那和 @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)

  • 性能好(重复输入直接命中)
  • 内存可能爆(无限缓存)

那什么时候该缓存,什么时候不该?

为什么“不加装饰器”时,虽然重复计算了,但内存更友好#

我们先明确几个前提概念:

1. 什么会占用内存?

内存主要被以下几类东西占用:

2. 不加缓存的函数,每次运行都怎样?

来看例子:

def square(x):
return x * x
square(100)
square(100)
square(100)

这里:

所以你每次都在重新计算,但计算完就忘记,不会留存历史记录 —— 内存友好!

3. 而加了 @lru_cache 会怎样?

@lru_cache(maxsize=None)
def square(x):
return x * x

这个时候:

核心区别

对比项不加缓存(无装饰器)加缓存(@lru_cache
每次调用是否计算是(总是重新算)否(重复输入直接命中)
结果是否保留❌ 不保留,临时变量用完即丢✅ 保留所有计算过的参数和结果
内存使用少,只在函数运行那一瞬间占点临时空间多,缓存不断积累,会一直驻留在内存中
适合场景输入多变、不重复,或对内存非常敏感的场景输入稳定/重复率高,希望提速的场景

结论一句话:

每次都算虽然慢,但干净缓存虽然快,但占空间

Python 默认“每次都算”正是因为这是最简单、最节省内存的方式。是否缓存,取决于你的性能 vs 内存的权衡。

这个也跟内存回收(GC)机制,或变量生命周期有关系,我们之后会继续展开。


做 Sequenzo 经常容易遇到的问题:算大矩阵时爆内存#

这是因为我们要存储整个矩阵数据本身,比如几千行几千列,这本身就会占用大量内存。

也就是说,即使没有 @lru_cache(没有手动缓存任何结果), 只要你把矩阵存在变量里(如 NumPy 数组、列表列表等),这块内存就不会被释放,除非你手动删除或变量生命周期结束

那在这种情况下,加 @lru_cache 有用吗?

通常没有用,甚至更糟。 原因:

  1. @lru_cache 是用于缓存函数的返回值
  2. 如果你的函数每次都接收不同的矩阵输入(比如一行行处理),那这些返回值就会不断被缓存
  3. 然而这些矩阵本身就很大,结果是:

在处理大矩阵时,正确做法应该是:

目标推荐做法
节省内存- 分批处理(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. 递归算法、动态规划#

@lru_cache
def 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 层的缓存#


4. 机器学习模型推理函数(特别是参数少的情况)#

但是,什么时候机器学习模型推理函数适合使用缓存?

前提是两个条件满足:

  1. 输入种类有限 / 重复率高
  2. 推理成本高(大模型、远程 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 就是这种需求中的“快速武器”🔧。


💡 小任务:尝试修改缓存大小(如 @lru_cache(maxsize=128)),看看是否有性能变化?