lru_cache:Python 缓存装饰器详解
引言
在 Python 开发中,你是否遇到过这样的情况:同一个函数被反复调用,每次都要重新计算,导致程序运行缓慢?特别是在处理递归、数据库查询、API 调用等场景时,这种重复计算的问题尤为突出。
lru_cache 就是解决这个问题的利器。它是 Python 标准库 functools 模块提供的一个装饰器,能够自动缓存函数的返回值,当函数被相同参数再次调用时,直接返回缓存结果,避免重复计算。
本文核心内容:
- LRU 缓存的核心概念与工作原理
lru_cache的基础用法与参数详解- 缓存的管理与监控方法
- 适用场景与最佳实践
- 常见误区与避坑指南
本文代码基于 Python 3.8+,建议读者具备 Python 装饰器的基础知识。文末附有完整代码示例和扩展阅读资源。
一、什么是 LRU 缓存?
1.1 基本概念
LRU 是 Least Recently Used(最近最少使用)的缩写,这是一种常用的缓存淘汰策略。
当缓存空间被占满时,LRU 策略会优先淘汰那些最久未被使用的数据,为新数据腾出空间。这种策略基于一个朴素的假设:如果数据最近被访问过,那么将来被访问的可能性也更高。
1.2 为什么需要缓存?
看一个简单的例子:
import time
def factorial(n):
"""计算阶乘(无缓存版本)"""
if n <= 1:
return 1
return n * factorial(n-1)
start = time.time()
print(factorial(30))
print(f"耗时: {time.time() - start:.4f}s")
# 再次调用,仍然要重新计算
start = time.time()
print(factorial(30))
print(f"耗时: {time.time() - start:.4f}s")
输出:
265252859812191058636308480000000
耗时: 0.0003s
265252859812191058636308480000000
耗时: 0.0003s
即使两次调用参数完全相同,函数仍然要执行完整的计算过程。如果函数更加复杂,这个浪费就会更加明显。
1.3 LRU 缓存如何工作?
lru_cache 维护了一个字典来存储缓存结果,同时用一个双向链表来记录访问顺序:
-
当函数被调用时,检查参数是否在缓存中
- 命中(Hit) :直接返回缓存值,并将该条目移到链表头部
- 未命中(Miss) :执行函数计算,将结果存入缓存
-
当缓存大小超过
maxsize限制时- 删除链表尾部的条目(最久未使用)
工作原理示意图:
┌─────────────────────────────────────┐
│ 调用函数 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 参数是否在缓存中? │
└──────────────────┬──────────────────┘
是 │ │ 否
▼ ▼
┌─────────────┐ ┌─────────────┐
│ 返回缓存值 │ │ 执行函数 │
│ 更新访问顺序 │ │ 存入缓存 │
└─────────────┘ └─────────────┘
│
▼
┌─────────────┐
│ 缓存是否已满?│
└──────┬──────┘
是 │
▼
┌─────────────┐
│淘汰最久未使用 │
│ 的条目 │
└─────────────┘
二、快速上手:基础用法
2.1 基本语法
from functools import lru_cache
@lru_cache(maxsize=128)
def my_function(x):
# 耗时操作
return result
2.2 实战示例:斐波那契数列
斐波那契数列是展示缓存效果的经典例子:
from functools import lru_cache
import time
@lru_cache(maxsize=128)
def fib_cached(n):
"""带缓存的斐波那契数列"""
if n < 2:
return n
return fib_cached(n-1) + fib_cached(n-2)
def fib_uncached(n):
"""不带缓存的斐波那契数列"""
if n < 2:
return n
return fib_uncached(n-1) + fib_uncached(n-2)
# 测试性能对比
n = 35
start = time.time()
result = fib_uncached(n)
print(f"无缓存: fib({n}) = {result}, 耗时: {time.time()-start:.4f}s")
start = time.time()
result = fib_cached(n)
print(f"有缓存: fib({n}) = {result}, 耗时: {time.time()-start:.4f}s")
# 查看缓存统计
print(fib_cached.cache_info())
输出:
无缓存: fib(35) = 9227465, 耗时: 2.8341s
有缓存: fib(35) = 9227465, 耗时: 0.0002s
CacheInfo(hits=67, misses=36, maxsize=128, currsize=36)
惊人差异:从 2.8 秒到 0.0002 秒,性能提升超过 10000 倍!
2.3 缓存统计信息解读
CacheInfo(hits=67, misses=36, maxsize=128, currsize=36)
| 字段 | 含义 |
|---|---|
hits=67 |
缓存命中次数 |
misses=36 |
缓存未命中次数(真正执行计算的次数) |
maxsize=128 |
最大缓存容量 |
currsize=36 |
当前缓存条目数 |
命中率 = hits / (hits + misses) = 67 / 103 ≈ 65.0%
三、参数详解与配置
3.1 maxsize 参数
控制缓存的最大条目数量:
@lru_cache(maxsize=128) # 最大缓存 128 个不同参数的结果
@lru_cache(maxsize=None) # 无限缓存(Python 3.9+ 推荐用 @cache)
@lru_cache(maxsize=1) # 只缓存最近一次调用的结果
如何选择合适的 maxsize?
# 场景1:已知数据量有限
@lru_cache(maxsize=1000) # 用户ID不超过1000
# 场景2:希望无限缓存但注意内存
@lru_cache(maxsize=None) # 如果参数组合有限
# 场景3:只缓存最常用的
@lru_cache(maxsize=256) # 保留最常用的256种参数组合
3.2 typed 参数
控制是否区分参数类型:
@lru_cache(maxsize=128, typed=False)
def test_no_typed(x):
print(f"计算: {x} ({type(x).__name__})")
return x
@lru_cache(maxsize=128, typed=True)
def test_typed(x):
print(f"计算: {x} ({type(x).__name__})")
return x
# typed=False(默认):1 和 1.0 视为相同参数
test_no_typed(1) # 计算: 1 (int)
test_no_typed(1.0) # 命中缓存,不执行
# typed=True:1 和 1.0 视为不同参数
test_typed(1) # 计算: 1 (int)
test_typed(1.0) # 计算: 1.0 (float)
3.3 参数必须可哈希
# ❌ 错误:使用不可哈希的类型作为参数
@lru_cache
def process_list(data): # data 是 list
return sum(data)
# TypeError: unhashable type: 'list'
# ✅ 正确:使用可哈希的类型
@lru_cache
def process_tuple(data): # data 是 tuple
return sum(data)
@lru_cache
def process_str(data): # data 是 str
return len(data)
可哈希类型对照表:
| 可哈希 ✅ | 不可哈希 ❌ |
|---|---|
| int, float, bool | list |
| str | dict |
| tuple | set |
| frozenset | 自定义对象(未实现 __hash__) |
| 不可变的自定义对象 |
四、缓存管理:查看与清理
4.1 cache_info():查看缓存统计
from functools import lru_cache
@lru_cache(maxsize=100)
def get_user(user_id):
# 模拟数据库查询
return f"User_{user_id}"
# 调用几次
get_user(1)
get_user(2)
get_user(1) # 命中缓存
# 查看统计
info = get_user.cache_info()
print(f"缓存命中: {info.hits}")
print(f"缓存未命中: {info.misses}")
print(f"缓存命中率: {info.hits/(info.hits+info.misses)*100:.1f}%")
print(f"当前缓存数: {info.currsize}/{info.maxsize}")
4.2 cache_clear():清空缓存
# 清空所有缓存
get_user.cache_clear()
# 清空后统计重置
print(get_user.cache_info())
# CacheInfo(hits=0, misses=0, maxsize=100, currsize=0)
清空缓存的典型场景:
- 数据源更新(如数据库内容变更)
- 内存压力较大时主动释放
- 测试或调试需要
4.3 缓存与内存监控
import sys
from functools import lru_cache
@lru_cache(maxsize=10000)
def get_large_data(key):
# 返回大对象
return [i for i in range(10000)]
# 监控内存使用
print(f"当前缓存条目: {get_large_data.cache_info().currsize}")
print(f"缓存对象大小: {sys.getsizeof(get_large_data.cache_info())}")
五、适用场景与最佳实践
5.1 场景1:递归优化
from functools import lru_cache
@lru_cache(maxsize=None)
def edit_distance(s1, s2):
"""计算编辑距离(Levenshtein距离)"""
if not s1:
return len(s2)
if not s2:
return len(s1)
if s1[0] == s2[0]:
return edit_distance(s1[1:], s2[1:])
return 1 + min(
edit_distance(s1[1:], s2), # 删除
edit_distance(s1, s2[1:]), # 插入
edit_distance(s1[1:], s2[1:]) # 替换
)
print(edit_distance("kitten", "sitting")) # 输出: 3
5.2 场景2:数据库查询
import sqlite3
from functools import lru_cache
@lru_cache(maxsize=1000)
def get_user_by_id(user_id):
"""通过用户ID查询用户信息"""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
result = cursor.fetchone()
conn.close()
return result
# 相同用户ID只查询一次数据库
user = get_user_by_id(1) # 查询数据库
user = get_user_by_id(1) # 从缓存读取
5.3 场景3:API响应缓存
import requests
from functools import lru_cache
@lru_cache(maxsize=500)
def fetch_weather(city):
"""获取天气数据"""
url = f"https://api.weather.com/city/{city}"
response = requests.get(url)
return response.json()
# 同一城市的天气数据只请求一次
weather = fetch_weather("Beijing")
5.4 场景4:RAG 系统相似度计算
from functools import lru_cache
import numpy as np
@lru_cache(maxsize=10000)
def compute_similarity(query_text, doc_text):
"""
计算查询与文档的相似度
实际场景中可能涉及向量化、模型推理等耗时操作
"""
# 模拟向量化与相似度计算
query_vector = np.random.randn(768)
doc_vector = np.random.randn(768)
return np.dot(query_vector, doc_vector) / (
np.linalg.norm(query_vector) * np.linalg.norm(doc_vector)
)
# 相同配对重复调用时直接返回缓存结果
sim = compute_similarity("什么是人工智能", "人工智能是...")
5.5 场景5:配置与规则加载
import json
from functools import lru_cache
@lru_cache(maxsize=10)
def load_config(config_file):
"""加载配置文件,避免重复读取文件"""
with open(config_file, 'r') as f:
return json.load(f)
# 多次读取同一配置文件只加载一次
config = load_config('app_config.json')
5.6 最佳实践总结
# ✅ 推荐:使用具名函数
@lru_cache
def get_data(key):
return expensive_operation(key)
# ❌ 不推荐:lambda 函数无法缓存
get_data = lru_cache(lambda x: expensive_operation(x))
六、常见问题与避坑指南
6.1 问题1:缓存不生效
import random
@lru_cache
def get_random():
"""❌ 错误:随机函数不应该被缓存"""
return random.random()
print(get_random()) # 0.1234
print(get_random()) # 0.1234 ← 返回相同值!不符合预期
解决方案:
- 纯函数:相同输入 → 相同输出
- 有副作用的函数不适合缓存
6.2 问题2:内存泄漏
@lru_cache(maxsize=None) # ❌ 无限缓存可能撑爆内存
def process_data(data):
return data * 1000
# 如果 data 有无数种可能,内存会不断增长
解决方案: 始终设置合理的 maxsize
@lru_cache(maxsize=1024) # ✅ 限制缓存大小
def process_data(data):
return data * 1000
6.3 问题3:使用不可哈希参数
# ❌ 错误
@lru_cache
def process_list(lst):
return sum(lst)
data = [1, 2, 3]
process_list(data) # TypeError
# ✅ 解决方案1:使用 tuple
@lru_cache
def process_tuple(tpl):
return sum(tpl)
data = (1, 2, 3)
process_tuple(data)
# ✅ 解决方案2:使用 json 序列化
@lru_cache
def process_json(data_str):
data = json.loads(data_str)
return sum(data)
process_json(json.dumps([1, 2, 3]))
6.4 问题4:类的实例方法缓存
class Calculator:
@lru_cache
def add(self, a, b): # ❌ self 不可哈希
return a + b
# TypeError: unhashable type: 'Calculator'
# ✅ 解决方案:使用 __slots__ 或静态方法
class Calculator:
__slots__ = [] # 使实例可哈希
@lru_cache
def add(self, a, b):
return a + b
七、高级技巧与扩展
7.1 Python 3.9+:@cache 装饰器
from functools import cache
@cache # 相当于 @lru_cache(maxsize=None)
def my_function(x):
return x * x
7.2 第三方库 cachetools
lru_cache 不支持设置过期时间(TTL),可以使用 cachetools:
pip install cachetools
from cachetools import TTLCache, cached, LFUCache
# TTL缓存:60秒后过期
ttl_cache = TTLCache(maxsize=100, ttl=60)
@cached(ttl_cache)
def get_latest_data():
return fetch_from_api()
# LFU缓存:最不经常使用淘汰
lfu_cache = LFUCache(maxsize=100)
@cached(lfu_cache)
def process_request(request_id):
return expensive_computation(request_id)
7.3 自定义缓存装饰器
from functools import wraps
from datetime import datetime, timedelta
def ttl_cache(ttl_seconds):
"""带 TTL 的缓存装饰器"""
def decorator(func):
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = (args, tuple(kwargs.items()))
if key in cache:
value, timestamp = cache[key]
if datetime.now() - timestamp < timedelta(seconds=ttl_seconds):
return value
result = func(*args, **kwargs)
cache[key] = (result, datetime.now())
return result
def clear():
cache.clear()
wrapper.clear = clear
return wrapper
return decorator
# 使用自定义缓存
@ttl_cache(ttl_seconds=60)
def get_data():
return fetch_from_api()
八、总结与展望
8.1 核心要点回顾
| 要点 | 说明 |
|---|---|
| 作用 | 缓存函数计算结果,避免重复计算 |
| 淘汰策略 | LRU(最近最少使用) |
| 关键参数 | maxsize(缓存大小)、typed(类型区分) |
| 管理方法 | cache_info()、cache_clear() |
| 适用场景 | 纯函数、计算密集、重复调用 |
| 核心限制 | 参数必须可哈希 |
8.2 什么时候应该使用 lru_cache?
✅ 推荐使用:
- 递归函数(如斐波那契、编辑距离)
- 数据库/API 查询(数据不频繁变化)
- 配置文件加载
- 耗时的数学计算
- RAG 系统中的相似度计算
❌ 不推荐使用:
- 依赖外部状态(时间、随机数)
- 有副作用(文件写入、发送邮件)
- 参数不可哈希
- 结果占用内存极大
8.3 进阶学习建议
- 理解缓存穿透:缓存未命中时大量请求同时打到数据库
- 学习缓存雪崩:大量缓存同时失效导致系统压力
- 探索分布式缓存:Redis、Memcached
- 了解缓存预热:系统启动时主动加载热点数据
附录:完整代码示例
from functools import lru_cache
import time
import random
# 示例1:斐波那契数列
@lru_cache(maxsize=128)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
# 示例2:数据库查询
@lru_cache(maxsize=1000)
def get_user(user_id):
# 模拟数据库查询
time.sleep(0.1)
return f"User_{user_id}"
# 示例3:API请求
@lru_cache(maxsize=500)
def fetch_weather(city):
# 模拟API请求
time.sleep(0.2)
return {"city": city, "temp": random.randint(-10, 40)}
if __name__ == "__main__":
# 测试斐波那契
print(f"fib(35) = {fib(35)}")
print(fib.cache_info())
# 测试用户查询
print(get_user(1))
print(get_user(1)) # 缓存命中
print(get_user.cache_info())
扩展阅读:

浙公网安备 33010602011771号