Python 装饰器进阶:带参数的装饰器与 functools.wraps 到底怎么写写装饰器的人多,但真正把「带参数的装饰器」和「元信息丢失」这两个坑填平的人少。你大概率遇到过这两件事:一是想给装饰器传个配置(比如重试几次、限流多少),结果三层嵌套的函数绕晕了;二是被装饰后的函数__name__变成了wrapper,日志和文档全乱套,Flask 路由甚至直接报重名。这篇把这两个问题一次讲透,给出能直接抄进项目的写法。先从最朴素的装饰器说起一个装饰器本质就是「接收一个函数、返回一个函数」:deflog_call(func):defwrapper(*args,**kwargs):print(f调用{func.__name__})returnfunc(*args,**kwargs)returnwrapperlog_calldefadd(a,b):returnabprint(add(1,2))# 先打印 调用 add,再返回 3log_call等价于add log_call(add)。到这里没问题,但两个隐患已经埋下了。坑一:被装饰后,函数的身份丢了接着上面的代码看一眼函数的元信息:print(add.__name__)# wrapper —— 不是 add 了!print(add.__doc__)# None —— 原来的文档字符串没了add现在指向的是wrapper,所以名字、文档、签名全变成了wrapper的。这在小脚本里无所谓,但一旦上规模就会出事:日志里全打印成wrapper,根本分不清是哪个函数在跑;用functools.lru_cache、pytest参数化这类依赖函数名的工具会错乱;Web 框架(Flask、FastAPI)常用函数名做路由端点名,多个函数都叫wrapper会直接抛「端点重复」异常。修复只要一行 —— 用functools.wraps把原函数的元信息复制到wrapper上:importfunctoolsdeflog_call(func):functools.wraps(func)# 关键:把 func 的 __name__/__doc__/__wrapped__ 拷给 wrapperdefwrapper(*args,**kwargs):print(f调用{func.__name__})returnfunc(*args,**kwargs)returnwrapperlog_calldefadd(a,b):两数相加returnabprint(add.__name__)# addprint(add.__doc__)# 两数相加functools.wraps本身也是个装饰器,它会把func的__name__、__doc__、__module__、__dict__拷过来,还会设置__wrapped__指回原函数,方便你在需要时拿到未包装的版本。写装饰器时无脑加上它,这是行业惯例。坑二:我想给装饰器传参数怎么办真实需求几乎都带配置:重试 3 次、限流每秒 5 次、日志级别设成 WARNING。但装饰器只接收一个「函数」参数,配置往哪塞?答案是再套一层 ——写一个「返回装饰器的函数」。也就是说,retry(times3)里的retry(times3)先执行,返回一个真正的装饰器,再去装饰函数:importfunctoolsimporttimedefretry(times3,delay0.5,exceptions(Exception,)):失败自动重试。times 次都失败才抛出最后一次异常。defdecorator(func):functools.wraps(func)defwrapper(*args,**kwargs):last_excNoneforattemptinrange(1,times1):try:returnfunc(*args,**kwargs)exceptexceptionsase:last_exce# 最后一次就不用再睡了,直接跳出去抛异常ifattempttimes:print(f[{func.__name__}] 第{attempt}次失败:{e},{delay}s 后重试)time.sleep(delay)raiselast_excreturnwrapperreturndecoratorretry(times3,delay1,exceptions(ConnectionError,))deffetch(url):raiseConnectionError(连接超时)fetch(http://example.com)理清这三层的执行顺序,整件事就通了:retry(times3, delay1, ...)先被调用,返回内层的decorator;decorator(fetch)再被调用,返回wrapper;最终fetch指向wrapper。记忆点:带参数的装饰器 三层函数,最外层收「装饰器参数」,中间层收「被装饰的函数」,最里层收「调用时的实参」。进阶:让装饰器「带不带参数都能用」上面的retry必须写成retry(),忘了括号写成retry就会报错,因为retry会把func当成times收走。想做到retry和retry(times5)都能用,可以判断第一个位置参数是不是可调用对象:importfunctoolsdefretry(_funcNone,*,times3,delay0.5):defdecorator(func):functools.wraps(func)defwrapper(*args,**kwargs):forattemptinrange(1,times1):try:returnfunc(*args,**kwargs)exceptException:ifattempttimes:raisereturnwrapper# retry 直接用:_func 是被装饰的函数,立刻装饰返回if_funcisnotNone:returndecorator(_func)# retry(times5) 用法:_func 是 None,返回装饰器等下一步returndecoratorretry# 没括号,走 _func 分支defa():...retry(times5)# 有括号,走 return decorator 分支defb():...*强制times、delay只能用关键字传,避免有人写retry(5)时把 5 误当成_func。这个模式在标准库里很常见,dataclasses.dataclass就是这么实现的。用 class 写装饰器(需要保存状态时更清晰)如果装饰器要累积状态(比如统计调用次数),用类比闭包更直观。此时用functools.update_wrapper代替wraps:importfunctoolsclassCountCalls:def__init__(self,func):functools.update_wrapper(self,func)# 等价于 wraps,拷贝元信息到实例self.funcfunc self.count0def__call__(self,*args,**kwargs):self.count1print(f{self.func.__name__}第{self.count}次调用)returnself.func(*args,**kwargs)CountCallsdefhello():print(hi)hello();hello()print(hello.count)# 2 —— 状态存在实例上,读取很自然wraps是给函数用的语法糖,update_wrapper是它底层调用的函数,类装饰器里手动调一下即可。小结写任何装饰器都加functools.wraps(func),否则被装饰函数的__name__/__doc__会丢,日志、缓存、Web 路由都可能出问题。带参数的装饰器 三层嵌套:最外层收装饰器参数,中间层收函数,最里层收调用实参;记住retry(times3)是「先调用retry拿到装饰器」。想做到「带不带括号都能用」,判断第一个参数是否可调用,并用*把配置项强制成关键字参数。需要保存状态时用类装饰器 functools.update_wrapper,比闭包更好读。一句话记忆:装饰器传参就多包一层函数,元信息丢失就functools.wraps补回来。
Python 装饰器进阶:带参数的装饰器与 functools.wraps 到底怎么写
Python 装饰器进阶:带参数的装饰器与 functools.wraps 到底怎么写写装饰器的人多,但真正把「带参数的装饰器」和「元信息丢失」这两个坑填平的人少。你大概率遇到过这两件事:一是想给装饰器传个配置(比如重试几次、限流多少),结果三层嵌套的函数绕晕了;二是被装饰后的函数__name__变成了wrapper,日志和文档全乱套,Flask 路由甚至直接报重名。这篇把这两个问题一次讲透,给出能直接抄进项目的写法。先从最朴素的装饰器说起一个装饰器本质就是「接收一个函数、返回一个函数」:deflog_call(func):defwrapper(*args,**kwargs):print(f调用{func.__name__})returnfunc(*args,**kwargs)returnwrapperlog_calldefadd(a,b):returnabprint(add(1,2))# 先打印 调用 add,再返回 3log_call等价于add log_call(add)。到这里没问题,但两个隐患已经埋下了。坑一:被装饰后,函数的身份丢了接着上面的代码看一眼函数的元信息:print(add.__name__)# wrapper —— 不是 add 了!print(add.__doc__)# None —— 原来的文档字符串没了add现在指向的是wrapper,所以名字、文档、签名全变成了wrapper的。这在小脚本里无所谓,但一旦上规模就会出事:日志里全打印成wrapper,根本分不清是哪个函数在跑;用functools.lru_cache、pytest参数化这类依赖函数名的工具会错乱;Web 框架(Flask、FastAPI)常用函数名做路由端点名,多个函数都叫wrapper会直接抛「端点重复」异常。修复只要一行 —— 用functools.wraps把原函数的元信息复制到wrapper上:importfunctoolsdeflog_call(func):functools.wraps(func)# 关键:把 func 的 __name__/__doc__/__wrapped__ 拷给 wrapperdefwrapper(*args,**kwargs):print(f调用{func.__name__})returnfunc(*args,**kwargs)returnwrapperlog_calldefadd(a,b):两数相加returnabprint(add.__name__)# addprint(add.__doc__)# 两数相加functools.wraps本身也是个装饰器,它会把func的__name__、__doc__、__module__、__dict__拷过来,还会设置__wrapped__指回原函数,方便你在需要时拿到未包装的版本。写装饰器时无脑加上它,这是行业惯例。坑二:我想给装饰器传参数怎么办真实需求几乎都带配置:重试 3 次、限流每秒 5 次、日志级别设成 WARNING。但装饰器只接收一个「函数」参数,配置往哪塞?答案是再套一层 ——写一个「返回装饰器的函数」。也就是说,retry(times3)里的retry(times3)先执行,返回一个真正的装饰器,再去装饰函数:importfunctoolsimporttimedefretry(times3,delay0.5,exceptions(Exception,)):失败自动重试。times 次都失败才抛出最后一次异常。defdecorator(func):functools.wraps(func)defwrapper(*args,**kwargs):last_excNoneforattemptinrange(1,times1):try:returnfunc(*args,**kwargs)exceptexceptionsase:last_exce# 最后一次就不用再睡了,直接跳出去抛异常ifattempttimes:print(f[{func.__name__}] 第{attempt}次失败:{e},{delay}s 后重试)time.sleep(delay)raiselast_excreturnwrapperreturndecoratorretry(times3,delay1,exceptions(ConnectionError,))deffetch(url):raiseConnectionError(连接超时)fetch(http://example.com)理清这三层的执行顺序,整件事就通了:retry(times3, delay1, ...)先被调用,返回内层的decorator;decorator(fetch)再被调用,返回wrapper;最终fetch指向wrapper。记忆点:带参数的装饰器 三层函数,最外层收「装饰器参数」,中间层收「被装饰的函数」,最里层收「调用时的实参」。进阶:让装饰器「带不带参数都能用」上面的retry必须写成retry(),忘了括号写成retry就会报错,因为retry会把func当成times收走。想做到retry和retry(times5)都能用,可以判断第一个位置参数是不是可调用对象:importfunctoolsdefretry(_funcNone,*,times3,delay0.5):defdecorator(func):functools.wraps(func)defwrapper(*args,**kwargs):forattemptinrange(1,times1):try:returnfunc(*args,**kwargs)exceptException:ifattempttimes:raisereturnwrapper# retry 直接用:_func 是被装饰的函数,立刻装饰返回if_funcisnotNone:returndecorator(_func)# retry(times5) 用法:_func 是 None,返回装饰器等下一步returndecoratorretry# 没括号,走 _func 分支defa():...retry(times5)# 有括号,走 return decorator 分支defb():...*强制times、delay只能用关键字传,避免有人写retry(5)时把 5 误当成_func。这个模式在标准库里很常见,dataclasses.dataclass就是这么实现的。用 class 写装饰器(需要保存状态时更清晰)如果装饰器要累积状态(比如统计调用次数),用类比闭包更直观。此时用functools.update_wrapper代替wraps:importfunctoolsclassCountCalls:def__init__(self,func):functools.update_wrapper(self,func)# 等价于 wraps,拷贝元信息到实例self.funcfunc self.count0def__call__(self,*args,**kwargs):self.count1print(f{self.func.__name__}第{self.count}次调用)returnself.func(*args,**kwargs)CountCallsdefhello():print(hi)hello();hello()print(hello.count)# 2 —— 状态存在实例上,读取很自然wraps是给函数用的语法糖,update_wrapper是它底层调用的函数,类装饰器里手动调一下即可。小结写任何装饰器都加functools.wraps(func),否则被装饰函数的__name__/__doc__会丢,日志、缓存、Web 路由都可能出问题。带参数的装饰器 三层嵌套:最外层收装饰器参数,中间层收函数,最里层收调用实参;记住retry(times3)是「先调用retry拿到装饰器」。想做到「带不带括号都能用」,判断第一个参数是否可调用,并用*把配置项强制成关键字参数。需要保存状态时用类装饰器 functools.update_wrapper,比闭包更好读。一句话记忆:装饰器传参就多包一层函数,元信息丢失就functools.wraps补回来。