从最简单的装饰器开始写,三个例子逐步升级,最后写出 Flask JWT 接口里的
login_required。每个例子都能直接运行,建议边看边敲。
引言:回顾通用模板
所有装饰器都是这个结构的变体,先记住它:
def my_decorator(func):
def wrapper(*args, **kwargs):
# 前置逻辑(原函数执行前)
result = func(*args, **kwargs)
# 后置逻辑(原函数执行后)
return result
return wrapper
三个位置可以填代码:执行前、执行后、以及决定要不要执行原函数。下面三个例子会分别用到这三个位置。
一、例子一:计时器,测量函数执行时间
最实用的入门装饰器。你想知道一个函数跑了多久,但不想在每个函数里都加计时代码。
1.1 先看不用装饰器的写法
import time
def slow_function():
time.sleep(1)
print("执行完毕")
start = time.time()
slow_function()
end = time.time()
print(f"耗时:{end - start:.2f} 秒")
如果有 10 个函数都要计时,每个都写一遍 start = time.time() … end = time.time(),需要重复写10次。
1.2 用装饰器的写法
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time() # 前置:记录开始时间
result = func(*args, **kwargs) # 执行原函数
end = time.time() # 后置:记录结束时间
print(f"{func.__name__} 耗时:{end - start:.2f} 秒")
return result
return wrapper
对照通用模板:前置逻辑是记录开始时间,后置逻辑是记录结束时间并打印差值。func.__name__ 拿到原函数的名字,这样打印出来就知道是哪个函数。
使用:
@timer
def slow_function():
time.sleep(1)
print("执行完毕")
@timer
def fast_function():
print("秒完")
slow_function()
fast_function()
执行完毕
slow_function 耗时:1.00 秒
秒完
fast_function 耗时:0.00 秒
@timer 往上一挂就行,想给哪个函数计时就挂哪个,不需要改函数本身的代码。不想计时了?把 @timer 删掉就行,函数本身一行都不用动。
二、例子二:日志记录,记录谁调用了什么
升级一下,写一个记录函数调用信息的装饰器:函数名、参数、返回值。
def log(func):
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__},参数:args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"{func.__name__} 返回:{result}")
return result
return wrapper
使用:
@log
def add(a, b):
return a + b
@log
def greet(name, greeting="你好"):
return f"{greeting},{name}"
add(1, 2)
greet("zhangsan")
greet("lisi", greeting="嗨")
调用 add,参数:args=(1, 2), kwargs={}
add 返回:3
调用 greet,参数:args=('zhangsan',), kwargs={}
greet 返回:你好,zhangsan
调用 greet,参数:args=('lisi',), kwargs={'greeting': '嗨'}
greet 返回:嗨,lisi
这里能看到 *args 和 **kwargs 的实际效果:add(1, 2) 的两个参数被 args 捕获为元组 (1, 2);greet("lisi", greeting="嗨") 的关键字参数被 kwargs 捕获为字典 {'greeting': '嗨'}。不管原函数长什么样,装饰器都能通用。
三、例子三:权限检查,决定"让不让执行"
前两个例子都是"前后加点逻辑,但原函数一定会执行"。这个例子不一样,装饰器要决定原函数能不能执行。这正是 login_required 的核心模式。
先写一个简单版:检查用户是不是管理员,是才让执行,不是就拒绝。
3.1 权限检查装饰器实现
current_user = {"name": "zhangsan", "role": "user"} # 模拟当前用户
def admin_required(func):
def wrapper(*args, **kwargs):
if current_user["role"] != "admin":
print(f"权限不足:{current_user['name']} 不是管理员")
return None # 不执行原函数,直接返回
return func(*args, **kwargs) # 是管理员,正常执行
return wrapper
和前两个例子的关键区别:wrapper 里有一个 if 判断,不满足条件就 return 了,根本不会调用 func()。 原函数被"拦"在了外面。
3.2 删除用户
@admin_required
def delete_user(user_id):
print(f"已删除用户{user_id}")
return True
-
如果是普通用户,即zhangsan
delete_user(123)# 权限不足:zhangsan 不是管理员 -
换成管理员:
current_user = {"name": "admin", "role": "admin"} delete_user(123)已删除用户123
是不是和 login_required 的模式很像了?login_required 做的就是同样的事,检查 Token 是否合法,合法才让请求进入接口函数,不合法就直接返回 401。
四、@wraps:修复装饰器的一个副作用
在进入 login_required 之前,还需要解决一个问题。
4.1 默认情况
看看装饰后函数的名字:
@timer
def slow_function():
"""这是一个慢函数"""
time.sleep(1)
print(slow_function.__name__)
print(slow_function.__doc__)
wrapper
None
名字变成了 wrapper,文档字符串也丢了。因为 slow_function 现在指向的是 wrapper 函数,而不是原来的 slow_function。
4.2 为什么这不只是"好看"的问题
这在 Flask 里会引发真实的 bug,Flask 用 __name__ 作为路由的 endpoint 名,如果两个视图函数装饰后名字都变成 wrapper,Flask 会抛出 AssertionError,提示 endpoint 重复注册。
4.3 用 @wraps 修复
从 functools 导入 wraps,加在 wrapper 上面:
from functools import wraps
def timer(func):
@wraps(func) # 加这一行
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 耗时:{end - start:.2f} 秒")
return result
return wrapper
@timer
def slow_function():
"""这是一个慢函数"""
time.sleep(1)
print(slow_function.__name__) # slow_function ← 名字保住了
print(slow_function.__doc__) # 这是一个慢函数 ← 文档也保住了
@wraps(func) 把原函数的名字、文档字符串等属性复制到 wrapper 上。写装饰器时永远加上 @wraps(func),这是最佳实践。
4.4 更新后的通用模板
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 前置逻辑
result = func(*args, **kwargs)
# 后置逻辑
return result
return wrapper
五、终极实战:写出 login_required
现在具备了所有知识,可以理解Flask JWT 登录接口里的 login_required 了。先回顾一下它做的事:
- 从请求头取
Authorization: Bearer <token> - 取出 Token,用密钥验签
- 验签通过 → 把用户信息挂到
request.user,放行 - 验签失败 → 直接返回 401,不执行接口函数
这就是例子三"权限检查"的模式:判断条件,决定让不让执行。只不过判断条件从"是不是管理员"变成了"Token 是否合法"。
from functools import wraps
from flask import request, jsonify
import jwt
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 第一步:从请求头取 Token
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return jsonify({"error": "缺少 Token"}), 401
token = auth_header.split(" ")[1]
# 第二步:验签
try:
data = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token 已过期"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "无效的 Token"}), 401
# 第三步:验签通过,放行
request.user = data
return func(*args, **kwargs)
return wrapper
对照通用模板看:
| 模板位置 | login_required 里做了什么 |
|---|---|
| 前置逻辑 | 取 Token、验签 |
| 决定是否执行原函数 | 验签失败 → return 401,不调用 func() |
func(*args, **kwargs) | 验签通过 → 执行接口函数(如 profile()) |
| 后置逻辑 | 无(接口返回什么就返回什么) |
-
使用时:
@app.route("/profile") @login_required def profile(): return jsonify({"name": request.user["username"]})
@login_required等价于profile = login_required(profile)。请求进来时,先执行wrapper验 Token,通过了才执行原来的profile()。 和你在例子三里写的
admin_required本质上完全一样,只是判断条件更复杂(验签而不是查角色)、返回值更规范(Flask 的 JSON 响应而不是 print)。
六、装饰器的执行顺序
Flask 接口上经常挂多个装饰器:
@app.route("/profile")
@login_required
def profile():
...
多个 @ 叠在一起,Python 从最靠近函数的那个开始,一层层往上包:
# Python 实际执行的等价形式:
profile = login_required(profile) # 第一步:login_required 先包裹
app.route("/profile")(profile) # 第二步:route 再把结果注册到路由表
请求来的时候,调用方向正好反过来:外层先执行,逐层往里走。
app.route 负责把这个包好的函数注册进路由表。当请求匹配到 /profile 时,Flask 调用注册的函数,也就是最外层的 login_required 的 wrapper(它先验 Token),通过后才调用内层的 profile。
我们用一个直观的例子来理解可能更为方便:

左边是定义阶段:Python 从最靠近函数的
@login_required开始包裹,再往外被@app.route包裹,像穿衣服,贴身的先穿。右边是调用阶段:与请求进来时方向相反,先经过最外层的路由匹配,再经过
login_required验 Token,最后才到达真正的profile() 函数,就像脱衣服,先脱外套。
七、总结
回顾一下这篇做了什么,从同一个模板出发,填不同的逻辑,写出了三种装饰器:
| 例子 | 在模板里填了什么 | 学到的新东西 |
|---|---|---|
| 计时器 | func() 前后记录时间 | 装饰器的基本写法 |
| 日志记录 | func() 前后打印参数和返回值 | *args, **kwargs 让装饰器适配任意函数 |
| 权限检查 | func() 前面加 if,不满足条件就不调用 | 装饰器可以拦截请求,不是非得调用原函数 |

1208

被折叠的 条评论
为什么被折叠?



