斐波那契数列加个装饰器,速度快了近五千倍:functools 三件套详解

「Python 进阶之路」系列 Day29

写在前面

Day05 讲装饰器时提到过写自定义装饰器要加 @functools.wraps,但没细讲为什么。今天把 functools 模块里最常用的三个工具------lru_cache、partial、wraps------一次讲透,各自解决的是完全不同类型的问题,别把它们混为一谈。


一、是什么:三个各管一段的工具

flowchart LR A[重复计算浪费时间] --> B[lru_cache<br/>缓存结果避免重算] C[需要预先固定部分参数] --> D[partial<br/>生成参数更少的新函数] E[装饰器让原函数元信息丢失] --> F[wraps<br/>把原函数信息复制回来]
  • lru_cache:缓存函数调用结果的装饰器,基于 LRU(最近最少使用)策略淘汰缓存
  • partial:固定一个函数的部分参数,返回一个参数更少的新可调用对象("偏函数")
  • wraps :修复自定义装饰器导致原函数元信息(__name__、__doc__ 等)丢失的问题

二、为什么

  • lru_cache 解决的是"纯函数被重复调用、每次都要重新算一遍"的浪费------用内存换时间
  • partial 解决的是"某些场景需要一个参数更少的简化版函数,自己手写包装函数太啰嗦"
  • wraps 解决的是"装饰器悄悄改变了函数的身份信息,导致调试和依赖函数名的逻辑出问题"

三、怎么用

1. lru_cache:用空间换时间,避免重复计算

递归/重复计算量大的函数会反复计算相同输入,lru_cache 把每次调用的参数和结果缓存起来,下次同样的参数直接返回缓存结果:

python 复制代码
import functools, time

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

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

# 实测 fib(30):
# 不加缓存: 0.0662s
# 加缓存:   0.000014s
# 快了 4876 倍!

不加缓存的朴素递归会重复计算大量相同的子问题(fib(28) 会在计算 fib(29) 和 fib(30) 的过程中被各算一遍),时间复杂度是指数级;加上缓存后每个子问题只会真正计算一次,退化成线性复杂度。

cache_info() 可以查看缓存命中情况:

python 复制代码
fib_cached.cache_clear()
fib_cached(20)
fib_cached(20)   # 重复调用
print(fib_cached.cache_info())
# CacheInfo(hits=19, misses=21, maxsize=None, currsize=21)

maxsize 参数限制缓存容量,超过容量后按 LRU 策略淘汰最久没被访问的结果:

python 复制代码
@functools.lru_cache(maxsize=2)
def square(x):
    return x * x

square(1)
square(2)
square(3)   # 缓存已满(maxsize=2),1被淘汰(最久没被访问)

info_before = square.cache_info()
square(1)   # 重新计算(因为1已被淘汰),不是缓存命中
info_after = square.cache_info()
print(info_before.misses, info_after.misses)
# 3 4 ------ misses增加了,说明square(1)确实被淘汰后重新计算了

2. partial:预先固定部分参数

某些场景需要"预先固定几个参数",生成一个更简单的调用接口,partial 比自己写一个包装函数更简洁:

python 复制代码
to_binary = functools.partial(int, base=2)
print(to_binary("1010"))   # 10,相当于 int("1010", base=2)

def power(base, exponent):
    return base ** exponent

square_fn = functools.partial(power, exponent=2)
cube_fn = functools.partial(power, exponent=3)
print(square_fn(5))   # 25
print(cube_fn(5))      # 125

常见应用场景:给多线程/多进程的 target 函数预先绑定部分参数、给回调函数固定一部分上下文参数,避免为每种参数组合单独写一个包装函数。

3. wraps:修复装饰器导致的元信息丢失

Day05 讲装饰器时提过要用 @functools.wraps,今天把它到底解决了什么问题讲清楚------自定义装饰器如果不用 wraps,被装饰函数的 __name__、__doc__ 等元信息会变成内部 wrapper 函数的:

python 复制代码
def my_decorator_no_wraps(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

def my_decorator_with_wraps(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator_no_wraps
def greet(name):
    """打个招呼"""
    return f"hello {name}"

@my_decorator_with_wraps
def greet2(name):
    """打个招呼"""
    return f"hello {name}"

print(greet.__name__, greet.__doc__)     # wrapper None ------ 元信息全丢了!
print(greet2.__name__, greet2.__doc__)    # greet2 打个招呼 ------ 正确保留

不用 wraps 时,greet.__name__ 变成了 "wrapper"、__doc__ 变成 None------这会影响调试(打印函数名时显示的是内部实现细节 wrapper,不是有意义的原函数名)、影响依赖函数名做路由/反射的框架、也会让 help() 查不到原本写好的文档字符串。

wraps 还会额外保留一个 __wrapped__ 属性,指向被装饰的原始函数,方便需要绕过装饰器逻辑直接访问原函数的场景:

python 复制代码
print(greet2.__wrapped__)   # <function greet2 at 0x...> ------ 指向最原始的greet2函数

四、面试追问

Q1:lru_cache 是怎么实现缓存的,用的什么淘汰策略?

用调用参数作为 key,把函数调用结果缓存起来,下次遇到同样的参数直接返回缓存值,不再重复计算;maxsize 限制缓存容量,超过容量后按 LRU(最近最少使用)策略淘汰最久没被访问的缓存项,把空间让给新的调用结果。

Q2:partial 解决了什么问题?

预先固定一个函数的部分参数,生成一个参数更少、调用更简单的新可调用对象,避免为每种固定参数组合单独手写一个包装函数,常用于多线程/多进程的目标函数预绑定参数、回调函数固定上下文等场景。

Q3:为什么自定义装饰器要加 @wraps?不加会有什么问题?

不加 wraps,被装饰函数的 __name__、__doc__ 等元信息会变成内部 wrapper 函数的,实测验证过 __name__ 会变成 "wrapper"、__doc__ 会丢失变成 None,这会影响调试可读性、影响依赖函数名做路由或反射的框架逻辑,也会让 help() 查不到原本写好的文档。加了 wraps 会把原函数的这些元信息正确复制过来,并额外保留 __wrapped__ 属性指向原函数。

Q4:lru_cache 适合什么场景,不适合什么场景?

适合纯函数(相同输入永远得到相同输出、没有副作用)且被重复调用、单次计算量较大的场景,比如递归计算、复杂查询;不适合参数不可哈希(比如传入 list)、结果会随时间或外部状态变化、或者函数本身有副作用(比如写文件、发请求)的场景,缓存这类函数的结果可能导致数据过时或行为错误。

Q5:functools.wraps 底层做了什么?

本质是调用 functools.update_wrapper,把原函数的 __name__、__doc__、__module__、__dict__ 等属性复制到 wrapper 函数上,覆盖 wrapper 自己原本的这些属性,并设置 wrapper 的 __wrapped__ 属性指向原函数,让外部代码看起来 wrapper 就是原函数本身。


下一篇预告

Day30 是模块六(常用数据结构与标准库进阶)的收官篇------常用魔法方法大盘点:__repr__、__eq__、__hash__、__len__ 这几个高频出现的魔法方法各自的作用和常见坑。

相关推荐
小小张说故事2 分钟前
Python 里 a is b 有时 True 有时 False?小整数缓存和常量折叠的底层真相
python
我的xiaodoujiao5 分钟前
Django 基础知识详细图文教程 16-Django 内置Admin认证系统
后端·python·django
ZGonce8197 分钟前
vLLM 使用实战:在 8GB 显卡上完成模型部署、并发压测与显存调优
python
程序员鱼皮8 分钟前
Personal Agent 是什么?为什么大厂都抢着做?连 DeepSeek Bot 都来了。。
前端·后端·ai编程
geovindu15 分钟前
rust: Chain of Responsibility Pattern
开发语言·后端·设计模式·rust·责任链模式·行为模式
m0_5873830015 分钟前
碰碰好评卡功能落地全流程实战指南 附核心逻辑与常见问题解决方案
java·前端·数据库·后端·struts·需求分析
dd聊技术17 分钟前
给检索改动装一盏红绿灯:评测门禁怎么建、怎么用
人工智能·后端
acd1200922 分钟前
Go 里 err 被不经意覆盖的几个现场
开发语言·后端·golang
XZ-07000123 分钟前
week8-代码解析2
python
合尘猫27 分钟前
给客服系统加一道闸:把「该转人工」从模糊判断变成可测拦截
人工智能·后端·程序员