找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3420

积分

0

好友

470

主题
发表于 9 小时前 | 查看: 3| 回复: 0

@cache 到底是什么?

@cache 是 Python 3.9 引入的内置装饰器(位于 functools 模块),它的核心作用是把函数的计算结果缓存起来。只要输入参数相同,就直接返回之前算过的结果,而不是重新计算一次。它特别适合那些计算代价高、但输入参数重复出现的场景。

像素化结构示意图

为什么要用 @cache?手动缓存的痛点

我们先看一个不使用 @cache 的经典例子:计算斐波那契数的递归函数。

def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

print(fib(35))  # 运行几秒甚至十几秒才能出结果

这个递归版本的斐波那契效率极低,因为它会反复计算相同的子问题,比如 fib(30) 会被计算无数次。

我们第一时间想到的优化方法是:加个字典手动缓存。

cache_dict = {}
def fib(n):
    if n in cache_dict:
        return cache_dict[n]
    if n <= 1:
        result = n
    else:
        result = fib(n-1) + fib(n-2)
    cache_dict[n] = result
    return result

print(fib(35))  # 瞬间出结果

这样确实快了很多,但有几个明显的问题:

  1. 全局变量污染cache_dict 是全局的,如果多个函数都想缓存,就得写多个字典,管理起来很混乱。
  2. 代码侵入性强:每次写新函数都要手动加这一套“查缓存 → 计算 → 存缓存”的逻辑,重复代码很多。
  3. 不优雅:如果函数参数是列表、字典等可变对象,或者有关键字参数,手动处理会更麻烦。
  4. 维护麻烦:想加缓存上限、想清空缓存、想统计命中率,都得自己再写一堆代码。

有没有一种方法,能一行代码就让函数自动记住所有调用结果,而且不污染全局、不用改函数内部逻辑、还能处理各种参数类型?

答案就是 @cache(或它的升级版 @lru_cache)。

使用 @cache,一行代码的魔法

我们直接给上面的函数加一行装饰器:

from functools import cache

@cache
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

print(fib(35))   # 瞬间出结果
print(fib(40))   # 还是瞬间
print(fib(35))   # 直接从缓存返回,0计算

加了 @cache 后,第一次调用 fib(35) 时,它会正常递归计算,同时把每个子问题的结果存起来。第二次调用 fib(35)fib(30) 时,直接从内存缓存中取值,几乎零耗时。整个函数内部代码一行没改!

再看一个更实际的例子:模拟耗时操作。

from functools import cache
import time

@cache
def expensive_calc(n):
    print(f"正在计算 expensive_calc({n}) ...")
    time.sleep(2)  # 模拟2秒的昂贵计算
    return n * n

print(expensive_calc(10))   # 打印计算中... 等待2秒 → 100
print(expensive_calc(10))   # 直接返回 100(无打印、无等待)
print(expensive_calc(20))   # 打印计算中... 等待2秒 → 400

你会发现:相同参数的调用只真正执行一次,后面都是秒返回。

@cache 的几种常见写法对比

from functools import cache, lru_cache

# 1. 最简单无限缓存(Python 3.9+)
@cache
def func(a, b):
    ...

# 2. 带缓存上限的 LRU(最常用)
@lru_cache(maxsize=128)  # 默认128,设为None则为无限缓存
def func(a, b):
    ...

# 3. 带类型提示(推荐写法)
from functools import cache
@cache
def add(a: int, b: int) -> int:
    print(f"计算 {a} + {b}")
    return a + b

@cache 的运行逻辑彻底拆解

我们用下面这个简单的例子来一步步讲清楚它在底层怎么工作:

from functools import cache

@cache
def add(a, b):
    print(f"真正计算 {a} + {b}")
    return a + b

print(add(3, 4))   # 真正计算 3 + 4 → 7
print(add(3, 4))   # 直接返回 7(无打印)
print(add(5, 6))   # 真正计算 5 + 6 → 11

当你写 @cache 时,发生了这些事:

  1. 装饰器在定义时执行@cache 是一个装饰器,它把 add 函数包装成了一个带缓存的版本。
  2. 生成一个缓存字典:内部会创建一个字典(或 LRU 结构),键是参数的唯一化表示,值是计算结果。
  3. 参数如何变成 key?:将位置参数和关键字参数打包成 (args_tuple, kwargs_frozendict)。所有不可哈希的对象(如 listdict)作为参数时会报错(TypeError: unhashable type)。这就是为什么 @cache 要求参数必须可哈希(如 intstrtuple 等)。
  4. 调用流程
    • 调用 add(3, 4)
    • 先把参数转为 key:((3, 4), {})
    • 查缓存字典,命中了吗?
    • 命中 → 直接返回缓存结果
    • 未命中 → 真正执行原函数 add(3, 4),得到 7
    • ((3, 4), {}) : 7 存进缓存
    • 返回 7

为什么第二次调用不打印“真正计算”?因为第二次查缓存命中了,直接跳过了原函数体。

但需要注意几个关键点:

  • 函数参数必须是可哈希的。
  • @cache 的缓存会一直占用内存(除非使用 @lru_cache 并设置 maxsize)。
  • 不适合参数变化极大、调用次数极多的场景(缓存可能会占用过多内存)。

合理地使用 @cache 装饰器,可以让你在编写Python代码时避免大量重复计算,显著提升程序性能,同时保持代码的简洁与优雅。如果你对这类提升开发效率的技巧感兴趣,可以在云栈社区找到更多相关的深度讨论和实践分享。




上一篇:Redux设计哲学解析:状态管理的可预测性与Zustand的轻量便捷如何选择
下一篇:PostgreSQL 18 基准测试深度分析:Redis缓存层是否仍需保留?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-10 18:05 , Processed in 0.381989 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表