07-Python装饰器从入门到源码(下)-带参数装饰器与wraps
文章目录
- Python 装饰器从入门到源码(下)——@wraps、带参数的装饰器
- 导入语
- 1 ~> `functools.wraps`——别让你的函数丢了身份
- 1.1 问题根源
- 1.2 解决方案:`@wraps`
- 1.3 `@wraps` 做了什么
- 2 ~> 带参数的装饰器——为什么你需要三层嵌套
- 2.1 场景:可配置的重试装饰器
- 2.2 逐步推导
- 3 ~> 实战装饰器一:权限校验
- 4 ~> 实战装饰器二:Django `@login_required` 的简化版
- 5 ~> 装饰器为什么优先选装饰器而不是直接改函数原因总结
- 思考 && 总结
- 结尾
Python 装饰器从入门到源码(下)——@wraps、带参数的装饰器
📖文章简介:上篇讲完了闭包和第一个装饰器,下篇解决两个高频问题:(1) 用了装饰器后函数的__name__和__doc__为什么会丢,以及functools.wraps怎么修;(2) 带参数的装饰器到底是怎么工作的——为什么需要三层嵌套函数。附带三个实战装饰器:权限校验装饰器、带重试次数的网络请求装饰器、Django 的@login_required简化实现。每个装饰器都有完整的逐步拆解和可执行代码。
🎬 个人主页:源码骑士
❄专栏传送门:《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
上篇我们写了第一个装饰器——计时器。看着很完美。但有个问题:
@timerdefadd(a,b):"""计算两个数的和"""returna+bprint(add.__name__)# 输出:wrapper ← ???不是 addprint(add.__doc__)# 输出:None ← 文档没了用了装饰器之后,函数的元信息全丢了。如果你在用 Flask 写路由@app.route('/'),__name__丢了影响还不大;但如果你的日志系统靠__name__区分函数来源、或者你写的装饰器被第三方工具用__doc__生成 API 文档时——这种信息丢失就是隐藏的生产事故。这就是functools.wraps出场的原因。下篇我们解决这个问题,顺便把带参数的装饰器也彻底搞明白。
1 ~>functools.wraps——别让你的函数丢了身份
1.1 问题根源
deftimer(func):defwrapper(*args,**kwargs):result=func(*args,**kwargs)returnresultreturnwrappertimer(add)返回的是wrapper函数。从此add变量指向的是wrapper。所以add.__name__自然是'wrapper'。
1.2 解决方案:@wraps
fromfunctoolsimportwrapsdeftimer(func):@wraps(func)# ← 关键!把 func 的元信息拷贝给 wrapperdefwrapper(*args,**kwargs):result=func(*args,**kwargs)returnresultreturnwrapper@timerdefadd(a,b):"""计算两个数的和"""returna+bprint(add.__name__)# 输出:add ✓print(add.__doc__)# 输出:计算两个数的和 ✓1.3@wraps做了什么
本质上,@wraps(func)等价于:
wrapper.__name__=func.__name__ wrapper.__doc__=func.__doc__ wrapper.__module__=func.__module__ wrapper.__dict__.update(func.__dict__)wrapper.__wrapped__=func# 记录原始函数引用文档、函数名、模块名、字典属性……全都从原函数复制过来。__wrapped__属性还保留了对原始函数的引用——某些调试工具靠它找回被装饰前的函数。
2 ~> 带参数的装饰器——为什么你需要三层嵌套
2.1 场景:可配置的重试装饰器
@retry(times=3,delay=1)# 你想传参数:重试3次,每次间隔1秒defcall_api():pass普通装饰器是timer(func)。带参数的装饰器是retry(times=3)(func)——先调用retry(times=3)拿到一个装饰器,再把func传进去。
2.2 逐步推导
importtimedefretry(times,delay):defdecorator(func):# 最外层返回真正的装饰器@wraps(func)defwrapper(*args,**kwargs):forattemptinrange(times):try:returnfunc(*args,**kwargs)exceptExceptionase:ifattempt==times-1:raise# 最后一次也失败了,抛异常time.sleep(delay)returnwrapperreturndecorator@retry(times=3,delay=0.5)defunstable_network_call():importrandomifrandom.random()<0.7:raiseConnectionError("网络超时")return"成功"三层嵌套的含义:
第1层 retry(times, delay)→ 接收装饰器参数(配置) 第2层 decorator(func)→ 接收被装饰的函数 第3层 wrapper(*args, **kwargs)→ 接收函数的调用参数带参数的装饰器本质上是一个"返回装饰器的函数"。@retry(times=3, delay=0.5)等价于:
unstable_network_call=retry(times=3,delay=0.5)(unstable_network_call)3 ~> 实战装饰器一:权限校验
fromfunctoolsimportwrapsdefrequire_role(role):defdecorator(func):@wraps(func)defwrapper(user,*args,**kwargs):ifuser.get("role")!=role:raisePermissionError(f"需要{role}权限,当前为{user.get('role')}")returnfunc(user,*args,**kwargs)returnwrapperreturndecorator@require_role("admin")defdelete_user(user,user_id):returnf"删除用户{user_id}"admin={"name":"张三","role":"admin"}normal={"name":"李四","role":"user"}print(delete_user(admin,42))# ✓ 删除用户 42# delete_user(normal, 42) # ❌ PermissionError: 需要 admin 权限,当前为 user你不需要在每个敏感接口里写if user.role != "admin",一行@require_role("admin")搞定。
4 ~> 实战装饰器二:Django@login_required的简化版
Django 的@login_required装饰器就是"带条件判断的闭包",核心逻辑拆出来很简单:
fromfunctoolsimportwrapsdeflogin_required(func):@wraps(func)defwrapper(request,*args,**kwargs):ifnotrequest.get("is_authenticated"):return{"error":"未登录","redirect":"/login/"}returnfunc(request,*args,**kwargs)returnwrapper@login_requireddefprofile(request):return{"name":"张三","age":28}print(profile({"is_authenticated":True}))# {'name': '张三', 'age': 28}print(profile({"is_authenticated":False}))# {'error': '未登录', 'redirect': '/login/'}5 ~> 装饰器为什么优先选装饰器而不是直接改函数原因总结
- 复用:一个装饰器可以适用于多个函数
- 解耦:权限逻辑和业务逻辑各自独立管理
- 可读:
@require_role("admin")比函数内部一堆 if 清晰得多 - 可组合:可以叠多个装饰器:
@login_required @require_role("admin") @log_api_call
思考 && 总结
下篇两个核心知识点:
@wraps(func)不能省。装饰器返回的是wrapper函数,如果不做信息恢复,原函数的__name__、__doc__全部丢失。这是生产环境日志和文档的暗坑。- 带参数的装饰器 = 三层嵌套函数。最外层接收装饰器参数(配置),中间层接收函数,最内层接收调用参数。理解这个结构之后,
@app.route('/')和@retry(times=3)的原理就完全通了。
结尾
各位小伙伴,装饰器上下篇到此全部结束。感谢阅读!
源码骑士 — Python 全栈 & 系统架构
👀关注:跟博主一起从源码视角深耕底层原理
❤️点赞:让优质内容被更多人看见
⭐收藏:核心知识点存好,随用随查
💬评论:分享你的经验或疑问,一起交流
🔄一键四连:不要忘记给博主"一键四连"哦!
🗡️寄语:技术之路,同行的人会让前路更有方向
结语:装饰器是 Python 的灵魂特性之一,上篇讲闭包,下篇讲应用——两篇读完,面试不怕。一键四连别忘了!
