Python 函数缓存实战:用 functools.lru_cache 给重复查询加速
背景
写工具脚本时经常遇到这种情况:一个函数输入固定、输出固定,却因为被循环反复调用而每次都重新算。比如根据用户 ID 查配置、按日期算统计口径、递归算斐波那契数列,输入相同结果必然相同,重复计算纯属浪费。自己手写一个 dict 做缓存当然可以,但要处理缓存上限、线程安全和统计信息就麻烦了。Python 标准库已经给了现成的工具:functools.lru_cache,一个装饰器就能给函数加上带淘汰策略的缓存。
实现思路
分三步走。第一步,找到“纯函数”:同样的输入永远返回同样的输出,中间不读写文件、不改全局状态,这样的函数才适合缓存。第二步,在函数上加 @lru_cache(maxsize=128),maxsize 控制缓存最多保留多少条记录,超出后按最久未使用淘汰;不传参数时默认 maxsize=128。第三步,用函数自带的 cache_info() 查看命中(hits)与未命中(misses),确认缓存真的在起作用;需要清空时调用 cache_clear()。
需要注意两点:一是被缓存函数的参数必须是可哈希的,列表、字典这类不可哈希类型传进去会直接报错;二是函数返回值如果会被调用方原地修改,缓存里存的也是同一份对象,改动会污染后续命中结果,这类情况不要缓存,或者返回副本。
代码
import time
from functools import lru_cache
@lru_cache(maxsize=128)
def slow_query(user_id: int) -> str:
"""模拟一次耗时查询:同样输入的结果固定,适合缓存。"""
time.sleep(0.2)
return f"user-{user_id}-profile"
def main() -> None:
start = time.perf_counter()
print(slow_query(7)) # 第 1 次:未命中,真实计算
print(slow_query(7)) # 第 2 次:命中缓存,直接返回
print(slow_query(8)) # 第 3 次:新输入,未命中
print(slow_query(7)) # 第 4 次:再次命中
elapsed = time.perf_counter() - start
print(f"4 次调用总耗时: {elapsed:.2f} 秒")
print(slow_query.cache_info())
if __name__ == "__main__":
main()
验证结果
已在本地 Python 3.12 实测。4 次调用总耗时约 0.40 秒:只有 user_id 为 7 和 8 的两次真正执行了 0.2 秒的“查询”,其余两次命中缓存几乎不耗时;若没有缓存,4 次调用需要约 0.80 秒。cache_info() 输出为 CacheInfo(hits=2, misses=2, maxsize=128, currsize=2),命中 2 次、未命中 2 次、当前缓存 2 条,与预期完全一致。
注意事项
- 只缓存纯函数:依赖当前时间、随机数、文件内容或数据库状态的函数,结果会随时间变化,缓存会返回过期数据。
- 参数必须可哈希:传列表或字典会报 TypeError,必要时先转成元组再调用。
- 返回可变对象要小心:调用方如果原地修改返回值,会污染缓存;可返回副本,或缓存不可变结果。
- 缓存不是越大越好:maxsize 设得太大,长期运行的程序会越占越多内存;128 是多数场景够用的起点,命中率低时再调大。
- 需要手动失效时调用 cache_clear(),比如底层数据更新后,让下一次调用重新计算。
- 点赞
- 收藏
- 关注作者
评论(0)