pin_drop当前位置:知识文库 ❯ 图文

Python functools.lru_cache详解 - LRU缓存优化函数性能

一、lru_cache 缓存概述

functools.lru_cache 是 functools 模块中最常用的性能优化装饰器,基于 LRU(Least Recently Used,最近最少使用)算法实现函数结果的自动缓存。

当使用相同的参数再次调用被装饰的函数时,lru_cache 会直接返回缓存的结果,而无需重新计算。这在递归算法、IO 密集型操作、数据库查询等场景中可以带来显著的性能提升。

LRU 算法的核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高。当缓存满时,最近最少使用的数据会被淘汰,为新数据腾出空间。

二、语法格式

代码示例

@functools.lru_cache(maxsize=128, typed=False)

三、参数详细说明

参数 类型 必填 默认值 说明
maxsize int 或 None 128 缓存的最大条目数。若为 None,则缓存无限制;若为 0,则禁用缓存
typed bool False 若为 True,则不同类型的参数(如 1 和 1.0)被视为不同的缓存键

四、返回值与附加方法

lru_cache 返回一个装饰器函数。被装饰的函数会获得以下额外方法:

  • cache_info():返回缓存统计信息,包含 hits(命中次数)、misses(未命中次数)、maxsize(最大缓存大小)、currsize(当前缓存大小)

  • cache_clear():清空缓存中的所有条目

五、代码示例详解

示例1:加速递归计算

斐波那契数列是展示 lru_cache 威力最经典的例子。不使用缓存的递归实现时间复杂度为 O(2^n),而使用缓存后降为 O(n)。

代码示例

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

# 计算第35个斐波那契数
print(f"fib(35) = {fibonacci(35)}")
print(f"缓存信息: {fibonacci.cache_info()}")

# 输出:
# fib(35) = 9227465
# 缓存信息: CacheInfo(hits=33, misses=36, maxsize=None, currsize=36)

示例2:缓存数据库查询结果

在实际开发中,lru_cache 常用于缓存耗时的 IO 操作,如数据库查询、API 请求等。下面通过模拟数据库查询来演示。

代码示例

from functools import lru_cache
import time

# 模拟耗时的数据库查询
@lru_cache(maxsize=100)
def query_user(user_id):
    """模拟查询用户信息(耗时操作)"""
    time.sleep(0.1)  # 模拟IO延迟
    users = {
        1: {'name': 'Alice', 'age': 25},
        2: {'name': 'Bob', 'age': 30},
        3: {'name': 'Charlie', 'age': 28},
    }
    return users.get(user_id, {'name': 'Unknown', 'age': 0})

# 第一次查询(缓存未命中)
start = time.time()
user1 = query_user(1)
print(f"第1次查询: {user1}, 耗时: {time.time() - start:.4f}秒")

# 第二次查询(缓存命中)
start = time.time()
user1_again = query_user(1)
print(f"第2次查询: {user1_again}, 耗时: {time.time() - start:.6f}秒")

print(f"缓存信息: {query_user.cache_info()}")

# 输出:
# 第1次查询: {'name': 'Alice', 'age': 25}, 耗时: 0.1005秒
# 第2次查询: {'name': 'Alice', 'age': 25}, 耗时: 0.000001秒
# 缓存信息: CacheInfo(hits=1, misses=1, maxsize=100, currsize=1)

可以看到,第一次查询耗时约 0.1 秒(模拟 IO 延迟),而第二次查询几乎瞬间完成,因为结果已被缓存。

示例3:typed 参数与缓存管理

typed 参数控制是否区分参数类型。同时,我们可以使用 cache_clear() 来清空缓存。

代码示例

from functools import lru_cache

# typed=True 时,1 和 1.0 被视为不同的键
@lru_cache(maxsize=32, typed=True)
def compute(x):
    print(f"计算 compute({x}, type={type(x).__name__})")
    return x * 2

print(compute(1))     # 缓存未命中
print(compute(1.0))   # 缓存未命中(typed=True,类型不同)
print(compute(1))     # 缓存命中

print(f"\n缓存信息: {compute.cache_info()}")

# 清空缓存
compute.cache_clear()
print(f"清空后: {compute.cache_info()}")

# 输出:
# 计算 compute(1, type=int)
# 2
# 计算 compute(1.0, type=float)
# 2.0
# 2
#
# 缓存信息: CacheInfo(hits=1, misses=2, maxsize=32, currsize=2)
# 清空后: CacheInfo(hits=0, misses=0, maxsize=32, currsize=0)

六、实际应用场景

  • 递归算法优化:斐波那契数列、动态规划等递归问题,缓存中间结果避免重复计算

  • IO 密集型操作:缓存数据库查询、API 请求、文件读取的结果,减少 IO 开销

  • 计算密集型操作:缓存复杂计算结果,如图像处理、数学运算等

七、注意事项

注意1lru_cache 使用函数参数作为缓存键,因此所有参数必须是可哈希的(hashable)。列表、字典、集合等不可哈希类型不能作为参数。

注意2:缓存会占用内存。对于返回大量数据的函数,请注意设置合理的 maxsize,避免内存溢出。

注意3lru_cache 不适合结果会随时间变化的函数(如查询实时数据),因为缓存不会自动失效。

提示maxsize=None 适合参数范围有限的函数;maxsize=128 是一个合理的默认值;maxsize=0 可用于禁用缓存(调试用)。

八、与相关方法对比

Python 中有多种方式可以实现函数缓存。以下是不同缓存方案的详细对比:

特性 lru_cache @cached_property 手动字典缓存 第三方 cachetools
缓存策略 LRU 属性缓存 无策略 多种策略
适用对象 函数 类属性 函数 函数
参数支持 支持 不支持 需手动 支持
过期机制 需手动 支持 TTL
线程安全 需手动
标准库

小贴士

如果你的缓存需要设置过期时间(TTL),可以考虑使用第三方库 cachetools,它提供了 TTLCache、TTLKeyCache 等带过期时间的缓存类型。如果需要更复杂的缓存策略,redismemcached 也是不错的选择。

九、本章小结

  • LRU 缓存lru_cache 基于 LRU 算法自动缓存函数结果,显著提升性能

  • 参数要求:参数必须是可哈希类型,缓存会占用内存

  • 缓存管理cache_info() 查看缓存统计,cache_clear() 清空缓存

  • 高级用法typed=True 区分不同类型的参数,maxsize=None 不限制缓存大小

十、练习题

练习1

使用 lru_cache 实现一个高效的阶乘计算函数 factorial(n),并比较缓存命中和未命中的次数。

练习2

编写一个使用 lru_cache 缓存的函数 get_weather(city),模拟查询城市天气(用 time.sleep 模拟网络延迟),验证第二次查询同一城市时是否命中缓存。

练习3

使用 lru_cache 实现一个计算组合数 C(n, k) 的函数,利用递归公式 C(n, k) = C(n-1, k-1) + C(n-1, k),并计算 C(30, 15)。

常见问题

lru_cache 的缓存是线程安全的吗?

是的,lru_cache 内部使用了线程锁来保证缓存操作的原子性,可以在多线程环境中安全使用。多个线程同时访问同一个缓存键时,只有一个线程会执行实际计算,其他线程会等待结果。

maxsize 应该设置为多少合适?

maxsize 的设置取决于具体场景。如果函数的参数组合有限且内存充足,可以设置为 None(无限缓存)。对于参数组合较多的函数,建议设置为 128、256 或 1024 等值,在缓存命中率和内存占用之间取得平衡。

如何让 lru_cache 的缓存过期?

lru_cache 本身不支持时间过期机制。如果需要缓存过期,可以:1)使用 cache_clear() 手动清空缓存;2)使用第三方库 cachetools 的 TTLCache;3)在函数内部加入时间判断逻辑,当数据过期时主动抛出异常绕过缓存。

lru_cache 可以缓存有副作用的函数吗?

不建议。lru_cache 适合纯函数(相同的输入总是产生相同的输出,且没有副作用)。如果函数有副作用(如写文件、修改全局变量),缓存会导致副作用只执行一次,后续调用直接返回缓存结果而不再执行副作用,这可能不是预期行为。

lru_cache 和 memoize 有什么区别?

memoize(记忆化)是一个通用的编程概念,指缓存函数返回结果以避免重复计算。lru_cache 是 Python 中 memoize 的一种具体实现,它额外提供了 LRU 淘汰策略来管理缓存大小。简单的 memoize 通常没有缓存大小限制。

标签: lru_cache 缓存优化 functools 性能优化 LRU算法 装饰器

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: Python functools.wraps详解 - 装饰器开发必备工具保留函数元信息 下一篇: Python functools.reduce用法详解:归约函数入门到实战

poll相关推荐