文章目录
- [Python 装饰器从入门到源码(下)------@wraps、带参数的装饰器](#Python 装饰器从入门到源码(下)——@wraps、带参数的装饰器)
-
- 导入语
- [1 ~> `functools.wraps`------别让你的函数丢了身份](#1 ~>
functools.wraps——别让你的函数丢了身份) -
- [1.1 问题根源](#1.1 问题根源)
- [1.2 解决方案:`@wraps`](#1.2 解决方案:
@wraps) - [1.3 `@wraps` 做了什么](#1.3
@wraps做了什么)
- [2 ~> 带参数的装饰器------为什么你需要三层嵌套](#2 ~> 带参数的装饰器——为什么你需要三层嵌套)
-
- [2.1 场景:可配置的重试装饰器](#2.1 场景:可配置的重试装饰器)
- [2.2 逐步推导](#2.2 逐步推导)
- [3 ~> 实战装饰器一:权限校验](#3 ~> 实战装饰器一:权限校验)
- [4 ~> 实战装饰器二:Django `@login_required` 的简化版](#4 ~> 实战装饰器二:Django
@login_required的简化版) - [5 ~> 装饰器为什么优先选装饰器而不是直接改函数原因总结](#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+篇,文章以源码拆解为特色,被读者评价为"看一篇胜过啃一周文档"
导入语
上篇我们写了第一个装饰器------计时器。看着很完美。但有个问题:
python
@timer
def add(a, b):
"""计算两个数的和"""
return a + b
print(add.__name__) # 输出:wrapper ← ???不是 add
print(add.__doc__) # 输出:None ← 文档没了
用了装饰器之后,函数的元信息全丢了。如果你在用 Flask 写路由 @app.route('/'),__name__ 丢了影响还不大;但如果你的日志系统靠 __name__ 区分函数来源、或者你写的装饰器被第三方工具用 __doc__ 生成 API 文档时------这种信息丢失就是隐藏的生产事故。这就是 functools.wraps 出场的原因。下篇我们解决这个问题,顺便把带参数的装饰器也彻底搞明白。
1 ~> functools.wraps------别让你的函数丢了身份
1.1 问题根源
python
def timer(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
timer(add) 返回的是 wrapper 函数。从此 add 变量指向的是 wrapper。所以 add.__name__ 自然是 'wrapper'。
1.2 解决方案:@wraps
python
from functools import wraps
def timer(func):
@wraps(func) # ← 关键!把 func 的元信息拷贝给 wrapper
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
@timer
def add(a, b):
"""计算两个数的和"""
return a + b
print(add.__name__) # 输出:add ✓
print(add.__doc__) # 输出:计算两个数的和 ✓
1.3 @wraps 做了什么
本质上,@wraps(func) 等价于:
python
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
wrapper.__module__ = func.__module__
wrapper.__dict__.update(func.__dict__)
wrapper.__wrapped__ = func # 记录原始函数引用
文档、函数名、模块名、字典属性......全都从原函数复制过来。__wrapped__ 属性还保留了对原始函数的引用------某些调试工具靠它找回被装饰前的函数。
2 ~> 带参数的装饰器------为什么你需要三层嵌套
2.1 场景:可配置的重试装饰器
python
@retry(times=3, delay=1) # 你想传参数:重试3次,每次间隔1秒
def call_api():
pass
普通装饰器是 timer(func)。带参数的装饰器是 retry(times=3)(func)------先调用 retry(times=3) 拿到一个装饰器,再把 func 传进去。
2.2 逐步推导
python
import time
def retry(times, delay):
def decorator(func): # 最外层返回真正的装饰器
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == times - 1:
raise # 最后一次也失败了,抛异常
time.sleep(delay)
return wrapper
return decorator
@retry(times=3, delay=0.5)
def unstable_network_call():
import random
if random.random() < 0.7:
raise ConnectionError("网络超时")
return "成功"
三层嵌套的含义:
bash
第1层 retry(times, delay) → 接收装饰器参数(配置)
第2层 decorator(func) → 接收被装饰的函数
第3层 wrapper(*args, **kwargs) → 接收函数的调用参数
带参数的装饰器本质上是一个"返回装饰器的函数"。 @retry(times=3, delay=0.5) 等价于:
python
unstable_network_call = retry(times=3, delay=0.5)(unstable_network_call)
3 ~> 实战装饰器一:权限校验
python
from functools import wraps
def require_role(role):
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if user.get("role") != role:
raise PermissionError(f"需要 {role} 权限,当前为 {user.get('role')}")
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def delete_user(user, user_id):
return f"删除用户 {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 装饰器就是"带条件判断的闭包",核心逻辑拆出来很简单:
python
from functools import wraps
def login_required(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
if not request.get("is_authenticated"):
return {"error": "未登录", "redirect": "/login/"}
return func(request, *args, **kwargs)
return wrapper
@login_required
def profile(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 的灵魂特性之一,上篇讲闭包,下篇讲应用------两篇读完,面试不怕。一键四连别忘了!