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 开销

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

七、注意事项

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

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

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

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

八、与相关方法对比

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

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

小贴士

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

九、本章小结

  • 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相关推荐