「Python 进阶之路」系列 Day29写在前面Day05 讲装饰器时提到过写自定义装饰器要加functools.wraps但没细讲为什么。今天把functools模块里最常用的三个工具——lru_cache、partial、wraps——一次讲透各自解决的是完全不同类型的问题别把它们混为一谈。一、是什么三个各管一段的工具重复计算浪费时间lru_cache缓存结果避免重算需要预先固定部分参数partial生成参数更少的新函数装饰器让原函数元信息丢失wraps把原函数信息复制回来lru_cache缓存函数调用结果的装饰器基于 LRU最近最少使用策略淘汰缓存partial固定一个函数的部分参数返回一个参数更少的新可调用对象“偏函数”wraps修复自定义装饰器导致原函数元信息__name__、__doc__等丢失的问题二、为什么lru_cache解决的是纯函数被重复调用、每次都要重新算一遍的浪费——用内存换时间partial解决的是某些场景需要一个参数更少的简化版函数自己手写包装函数太啰嗦wraps解决的是装饰器悄悄改变了函数的身份信息导致调试和依赖函数名的逻辑出问题三、怎么用1. lru_cache用空间换时间避免重复计算递归/重复计算量大的函数会反复计算相同输入lru_cache把每次调用的参数和结果缓存起来下次同样的参数直接返回缓存结果importfunctools,timedeffib_no_cache(n):ifn2:returnnreturnfib_no_cache(n-1)fib_no_cache(n-2)functools.lru_cache(maxsizeNone)deffib_cached(n):ifn2:returnnreturnfib_cached(n-1)fib_cached(n-2)# 实测 fib(30)# 不加缓存: 0.0662s# 加缓存: 0.000014s# 快了 4876 倍不加缓存的朴素递归会重复计算大量相同的子问题fib(28)会在计算fib(29)和fib(30)的过程中被各算一遍时间复杂度是指数级加上缓存后每个子问题只会真正计算一次退化成线性复杂度。cache_info()可以查看缓存命中情况fib_cached.cache_clear()fib_cached(20)fib_cached(20)# 重复调用print(fib_cached.cache_info())# CacheInfo(hits19, misses21, maxsizeNone, currsize21)maxsize参数限制缓存容量超过容量后按 LRU 策略淘汰最久没被访问的结果functools.lru_cache(maxsize2)defsquare(x):returnx*x square(1)square(2)square(3)# 缓存已满(maxsize2)1被淘汰(最久没被访问)info_beforesquare.cache_info()square(1)# 重新计算(因为1已被淘汰)不是缓存命中info_aftersquare.cache_info()print(info_before.misses,info_after.misses)# 3 4 —— misses增加了说明square(1)确实被淘汰后重新计算了2. partial预先固定部分参数某些场景需要预先固定几个参数生成一个更简单的调用接口partial比自己写一个包装函数更简洁to_binaryfunctools.partial(int,base2)print(to_binary(1010))# 10相当于 int(1010, base2)defpower(base,exponent):returnbase**exponent square_fnfunctools.partial(power,exponent2)cube_fnfunctools.partial(power,exponent3)print(square_fn(5))# 25print(cube_fn(5))# 125常见应用场景给多线程/多进程的target函数预先绑定部分参数、给回调函数固定一部分上下文参数避免为每种参数组合单独写一个包装函数。3. wraps修复装饰器导致的元信息丢失Day05 讲装饰器时提过要用functools.wraps今天把它到底解决了什么问题讲清楚——自定义装饰器如果不用wraps被装饰函数的__name__、__doc__等元信息会变成内部wrapper函数的defmy_decorator_no_wraps(func):defwrapper(*args,**kwargs):returnfunc(*args,**kwargs)returnwrapperdefmy_decorator_with_wraps(func):functools.wraps(func)defwrapper(*args,**kwargs):returnfunc(*args,**kwargs)returnwrappermy_decorator_no_wrapsdefgreet(name):打个招呼returnfhello{name}my_decorator_with_wrapsdefgreet2(name):打个招呼returnfhello{name}print(greet.__name__,greet.__doc__)# wrapper None —— 元信息全丢了print(greet2.__name__,greet2.__doc__)# greet2 打个招呼 —— 正确保留不用wraps时greet.__name__变成了wrapper、__doc__变成None——这会影响调试打印函数名时显示的是内部实现细节wrapper不是有意义的原函数名、影响依赖函数名做路由/反射的框架、也会让help()查不到原本写好的文档字符串。wraps还会额外保留一个__wrapped__属性指向被装饰的原始函数方便需要绕过装饰器逻辑直接访问原函数的场景print(greet2.__wrapped__)# function greet2 at 0x... —— 指向最原始的greet2函数四、面试追问Q1lru_cache 是怎么实现缓存的用的什么淘汰策略用调用参数作为 key把函数调用结果缓存起来下次遇到同样的参数直接返回缓存值不再重复计算maxsize限制缓存容量超过容量后按 LRU最近最少使用策略淘汰最久没被访问的缓存项把空间让给新的调用结果。Q2partial 解决了什么问题预先固定一个函数的部分参数生成一个参数更少、调用更简单的新可调用对象避免为每种固定参数组合单独手写一个包装函数常用于多线程/多进程的目标函数预绑定参数、回调函数固定上下文等场景。Q3为什么自定义装饰器要加 wraps不加会有什么问题不加wraps被装饰函数的__name__、__doc__等元信息会变成内部wrapper函数的实测验证过__name__会变成wrapper、__doc__会丢失变成None这会影响调试可读性、影响依赖函数名做路由或反射的框架逻辑也会让help()查不到原本写好的文档。加了wraps会把原函数的这些元信息正确复制过来并额外保留__wrapped__属性指向原函数。Q4lru_cache 适合什么场景不适合什么场景适合纯函数相同输入永远得到相同输出、没有副作用且被重复调用、单次计算量较大的场景比如递归计算、复杂查询不适合参数不可哈希比如传入 list、结果会随时间或外部状态变化、或者函数本身有副作用比如写文件、发请求的场景缓存这类函数的结果可能导致数据过时或行为错误。Q5functools.wraps 底层做了什么本质是调用functools.update_wrapper把原函数的__name__、__doc__、__module__、__dict__等属性复制到 wrapper 函数上覆盖 wrapper 自己原本的这些属性并设置 wrapper 的__wrapped__属性指向原函数让外部代码看起来 wrapper 就是原函数本身。下一篇预告Day30 是模块六常用数据结构与标准库进阶的收官篇——常用魔法方法大盘点__repr__、__eq__、__hash__、__len__这几个高频出现的魔法方法各自的作用和常见坑。