📚 缓存系列 · 第 2 篇(共 7 篇)📌 上一篇:缓存为什么能提速?Python 带你搞懂背后的原理📌 本篇主角:functools.lru_cache —— Python 自带的缓存装饰器📌 你将学到:一行装饰器给函数加缓存、控制缓存大小、查看命中率、实战案例📌 难度指数:⭐(零基础可学)
上一篇我们用手写字典实现了缓存,明白了:
缓存 = 算过存起来,下次直接用
但手写缓存有个问题——每个函数都要写一遍"查缓存、存缓存、判过期"的逻辑,很烦。
Python 早就想到了:标准库自带缓存装饰器,一行搞定。
from functools import lru_cache@lru_cache()def expensive(x): ...
加个装饰器,函数自动获得缓存能力。
一、lru_cache 是什么?
lru_cache 是 Python 标准库 functools 里的装饰器。
LRU = Least Recently Used(最近最少使用)
它帮你自动管理:
最简用法
from functools import lru_cacheimport time@lru_cache(maxsize=128) # ← 加这一行就完事def get_user(user_id): """模拟耗时 0.2 秒的数据库查询""" time.sleep(0.2) return {"id": user_id, "name": f"用户{user_id}"}
测试速度:
# 第一次调用(真的查询,耗时 0.2s)start = time.time()get_user(1)print(f"第1次: {time.time()-start:.2f}s")# 第二、三次调用(命中缓存,秒回)start = time.time()get_user(1)get_user(1)print(f"第2、3次: {time.time()-start:.2f}s")
输出:
第1次: 0.20s第2、3次: 0.00s
💡 效果:3 次调用,只有第 1 次真的执行了函数,后面 2 次直接读缓存。
二、maxsize:缓存能存多少?
maxsize 控制缓存大小:
# 最多缓存 128 个结果@lru_cache(maxsize=128)def f(x): ...# 无上限(适合确定性的纯函数)@lru_cache(maxsize=None)def f(x): ...
缓存满了会怎样?—— LRU 淘汰
假设 maxsize=3,连续调用不同参数:
@lru_cache(maxsize=3)def f(x): print(f" 计算 f({x})") return x * 2# 依次调用f(1) # 计算,缓存 [1]f(2) # 计算,缓存 [1,2]f(3) # 计算,缓存 [1,2,3] ← 满了f(4) # 计算,缓存 [2,3,4] ← 1 最久没用,被淘汰f(1) # 计算!1 已被淘汰,重新算
输出:
计算 f(1) 计算 f(2) 计算 f(3) 计算 f(4) 计算 f(1) ← 1 被淘汰了,要重新算
💡 LRU 的淘汰规则:最久没被用过的先删。 因为"最近用过的可能马上还要用"。
maxsize 怎么定?
三、查看缓存命中情况:cache_info()
测试缓存好不好用,看命中率:
from functools import lru_cache@lru_cache(maxsize=128)def f(x): return x * 2# 调用 5 次,其中 2 次重复f(1); f(2); f(3); f(1); f(2)print(f.cache_info())
输出:
CacheInfo(hits=2, misses=3, maxsize=128, currsize=3)
| |
|---|
hits | |
misses | |
maxsize | |
currsize | |
命中率 = hits / (hits + misses) = 2/5 = 40%
💡 测试时用 cache_info() 验证缓存是否生效——如果命中率是 0,说明缓存根本没起作用。
四、清空缓存:cache_clear()
数据更新了,缓存还是旧的怎么办?手动清空:
@lru_cache(maxsize=128)def get_user(user_id): return {"id": user_id, "name": f"用户{user_id}"}# 业务:用户改名了def update_user_name(user_id, new_name): # 更新数据库 # ... # 清掉这个用户的缓存(下次查询重新加载) get_user.cache_clear()# 也可以只清单个缓存(Python 3.8+)# get_user.cache_clear() # 全清
五、实战 1:斐波那契——缓存威力最大化的经典
斐波那契数列:F(n) = F(n-1) + F(n-2),用递归实现会重复计算大量子问题:
# ❌ 无缓存:指数级重复计算def fib(n): if n <= 1: return n return fib(n-1) + fib(n-2)# ✅ 有缓存:每个 n 只算一次from functools import lru_cache@lru_cache(maxsize=None)def fib_cached(n): if n <= 1: return n return fib_cached(n-1) + fib_cached(n-2)
对比测试:
import timestart = time.time()fib(35)print(f"fib(35) 无缓存: {time.time()-start:.3f}s")start = time.time()fib_cached(35)print(f"fib(35) 有缓存: {time.time()-start:.4f}s")print(f"命中统计: {fib_cached.cache_info()}")
输出:
fib(35) 无缓存: 1.023s ← 慢fib(35) 有缓存: 0.0000s ← 快了几百倍命中统计: CacheInfo(hits=33, misses=36, maxsize=None, currsize=36)
🔥 同一个函数,加一行装饰器,从 1 秒变成 0.0001 秒——这就是缓存的力量。
六、实战 2:缓存数据库查询结果
最常见的业务场景:同一份数据被反复查询。
from functools import lru_cacheimport time# 模拟数据库USER_DB = { 1: {"name": "张三", "city": "北京"}, 2: {"name": "李四", "city": "上海"},}@lru_cache(maxsize=128)def get_user(user_id): """查询用户(模拟耗时 100ms)""" time.sleep(0.1) return USER_DB.get(user_id)# 模拟 100 次请求,但只有 3 个不同用户for i in range(100): get_user(i % 3 + 1)print(f"命中统计: {get_user.cache_info()}")# CacheInfo(hits=97, misses=3, maxsize=128, currsize=3)# 100 次请求,只有 3 次真正查了数据库!
💡 命中 97 次、未命中 3 次——100 次请求只查了 3 次数据库,这就是生产环境缓存的威力。
七、什么时候不该用 lru_cache?
lru_cache 不是万能的,有副作用或参数不可哈希的函数不能用:
❌ 1. 有副作用的函数
@lru_cache()def send_sms(phone): """❌ 错误:发短信有副作用,不能缓存""" requests.post(f"http://sms.api/send?phone={phone}") return "ok"# 缓存后:第二次调用不会真的发短信!send_sms("13800000000") # 第1次:真发send_sms("13800000000") # 第2次:命中缓存,没发!❌
❌ 2. 参数不可哈希
@lru_cache()def f(data: list): """❌ 错误:list 不可哈希,lru_cache 用不了""" return sum(data)# TypeError: unhashable type: 'list'
❌ 3. 结果依赖全局状态
count = 0@lru_cache()def get_count(): """❌ 错误:结果会变,缓存了就错了""" return countcount = 10print(get_count()) # 0(缓存了旧值)print(get_count()) # 0(还是旧值,永远不会变成 10)
一句话:只缓存"同样的输入 → 同样的输出"的纯函数。
八、lru_cache vs 手写缓存
总结
| |
|---|
| lru_cache | |
| maxsize | |
| cache_info() | |
| cache_clear() | |
| 适用场景 | |
lru_cache 是 Python 最简单实用的缓存工具,没有之一。
下一篇我们挑战一下:不用 lru_cache,自己手写一个带过期时间的 LRU 缓存,彻底搞懂它的内部原理。
📝 下篇预告:手写一个缓存:从字典到 LRU,一步步搞定
关注我,软件测试实战干货持续更新 🚀