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 进阶学习建议

  1. 理解缓存穿透:缓存未命中时大量请求同时打到数据库
  2. 学习缓存雪崩:大量缓存同时失效导致系统压力
  3. 探索分布式缓存:Redis、Memcached
  4. 了解缓存预热:系统启动时主动加载热点数据

附录:完整代码示例

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())

扩展阅读:

posted @ 2026-08-15 11:11  静心笃行。  阅读(1)  评论(0)    收藏  举报