news 2026/7/9 18:08:42

Python 装饰器进阶:带参数的装饰器与 functools.wraps 到底怎么写

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python 装饰器进阶:带参数的装饰器与 functools.wraps 到底怎么写

Python 装饰器进阶:带参数的装饰器与 functools.wraps 到底怎么写

写装饰器的人多,但真正把「带参数的装饰器」和「元信息丢失」这两个坑填平的人少。你大概率遇到过这两件事:一是想给装饰器传个配置(比如重试几次、限流多少),结果三层嵌套的函数绕晕了;二是被装饰后的函数__name__变成了wrapper,日志和文档全乱套,Flask 路由甚至直接报重名。

这篇把这两个问题一次讲透,给出能直接抄进项目的写法。

先从最朴素的装饰器说起

一个装饰器本质就是「接收一个函数、返回一个函数」:

deflog_call(func):defwrapper(*args,**kwargs):print(f"调用{func.__name__}")returnfunc(*args,**kwargs)returnwrapper@log_calldefadd(a,b):returna+bprint(add(1,2))# 先打印 "调用 add",再返回 3

@log_call等价于add = log_call(add)。到这里没问题,但两个隐患已经埋下了。

坑一:被装饰后,函数的身份丢了

接着上面的代码看一眼函数的元信息:

print(add.__name__)# wrapper —— 不是 add 了!print(add.__doc__)# None —— 原来的文档字符串没了

add现在指向的是wrapper,所以名字、文档、签名全变成了wrapper的。这在小脚本里无所谓,但一旦上规模就会出事:

  • 日志里全打印成wrapper,根本分不清是哪个函数在跑;
  • functools.lru_cachepytest参数化这类依赖函数名的工具会错乱;
  • 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)returnwrapper@log_calldefadd(a,b):"""两数相加"""returna+bprint(add.__name__)# addprint(add.__doc__)# 两数相加

functools.wraps本身也是个装饰器,它会把func__name____doc____module____dict__拷过来,还会设置__wrapped__指回原函数,方便你在需要时拿到未包装的版本。写装饰器时无脑加上它,这是行业惯例。

坑二:我想给装饰器传参数怎么办

真实需求几乎都带配置:重试 3 次、限流每秒 5 次、日志级别设成 WARNING。但装饰器只接收一个「函数」参数,配置往哪塞?

答案是再套一层 ——写一个「返回装饰器的函数」。也就是说,@retry(times=3)里的retry(times=3)先执行,返回一个真正的装饰器,再去装饰函数:

importfunctoolsimporttimedefretry(times=3,delay=0.5,exceptions=(Exception,)):"""失败自动重试。times 次都失败才抛出最后一次异常。"""defdecorator(func):@functools.wraps(func)defwrapper(*args,**kwargs):last_exc=Noneforattemptinrange(1,times+1):try:returnfunc(*args,**kwargs)exceptexceptionsase:last_exc=e# 最后一次就不用再睡了,直接跳出去抛异常ifattempt<times:print(f"[{func.__name__}] 第{attempt}次失败:{e},{delay}s 后重试")time.sleep(delay)raiselast_excreturnwrapperreturndecorator@retry(times=3,delay=1,exceptions=(ConnectionError,))deffetch(url):raiseConnectionError("连接超时")fetch("http://example.com")

理清这三层的执行顺序,整件事就通了:

  1. retry(times=3, delay=1, ...)先被调用,返回内层的decorator;
  2. decorator(fetch)再被调用,返回wrapper;
  3. 最终fetch指向wrapper

记忆点:带参数的装饰器 = 三层函数,最外层收「装饰器参数」,中间层收「被装饰的函数」,最里层收「调用时的实参」。

进阶:让装饰器「带不带参数都能用」

上面的retry必须写成@retry(),忘了括号写成@retry就会报错,因为retry会把func当成times收走。想做到@retry@retry(times=5)都能用,可以判断第一个位置参数是不是可调用对象:

importfunctoolsdefretry(_func=None,*,times=3,delay=0.5):defdecorator(func):@functools.wraps(func)defwrapper(*args,**kwargs):forattemptinrange(1,times+1):try:returnfunc(*args,**kwargs)exceptException:ifattempt==times:raisereturnwrapper# @retry 直接用:_func 是被装饰的函数,立刻装饰返回if_funcisnotNone:returndecorator(_func)# @retry(times=5) 用法:_func 是 None,返回装饰器等下一步returndecorator@retry# 没括号,走 _func 分支defa():...@retry(times=5)# 有括号,走 return decorator 分支defb():...

*强制timesdelay只能用关键字传,避免有人写@retry(5)时把 5 误当成_func。这个模式在标准库里很常见,dataclasses.dataclass就是这么实现的。

用 class 写装饰器(需要保存状态时更清晰)

如果装饰器要累积状态(比如统计调用次数),用类比闭包更直观。此时用functools.update_wrapper代替@wraps:

importfunctoolsclassCountCalls:def__init__(self,func):functools.update_wrapper(self,func)# 等价于 @wraps,拷贝元信息到实例self.func=func self.count=0def__call__(self,*args,**kwargs):self.count+=1print(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(times=3)是「先调用retry拿到装饰器」。
  • 想做到「带不带括号都能用」,判断第一个参数是否可调用,并用*把配置项强制成关键字参数。
  • 需要保存状态时用类装饰器 +functools.update_wrapper,比闭包更好读。

一句话记忆:装饰器传参就多包一层函数,元信息丢失就functools.wraps补回来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/9 18:08:33

TotalSegmentator API实战指南:解锁医学图像分割的Python集成方案

TotalSegmentator API实战指南&#xff1a;解锁医学图像分割的Python集成方案 【免费下载链接】TotalSegmentator Tool for robust segmentation of >100 important anatomical structures in CT and MR images 项目地址: https://gitcode.com/gh_mirrors/to/TotalSegment…

作者头像 李华
网站建设 2026/7/9 18:07:03

A、B、C类IP地址怎么划分?一文学会判断是否属于同一个子网

对于网络工程师来说,IP地址可以说是每天都会接触到的基础知识。无论是给交换机配置管理地址、为服务器分配IP,还是排查网络故障,几乎都离不开IP地址和子网的概念。 不过,对于很多刚接触网络的朋友来说,看到 192.168.1.10、10.0.0.1、172.16.100.5 这些IP地址时,往往会产…

作者头像 李华
网站建设 2026/7/9 18:03:05

桌面智能体实战:从LLM工具调用到系统自动化工作流

&#x1f680; 30款热门AI模型一站整合&#xff0c;DeepSeek/GLM/Qwen 随心用&#xff0c;限时 5 折。 &#x1f449; 点击领海量免费额度 最近在技术社区里&#xff0c;一个名为“昔涟”的桌面智能体&#xff08;Agent&#xff09;项目引起了不小的讨论。很多开发者&#x…

作者头像 李华