【Python 装饰器】实战:从计时器到登录验证

从最简单的装饰器开始写,三个例子逐步升级,最后写出 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 了。先回顾一下它做的事:

  1. 从请求头取 Authorization: Bearer <token>
  2. 取出 Token,用密钥验签
  3. 验签通过 → 把用户信息挂到 request.user,放行
  4. 验签失败 → 直接返回 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,不满足条件就不调用装饰器可以拦截请求,不是非得调用原函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未收敛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值