07-Python装饰器从入门到源码(下)-带参数装饰器与wraps

文章目录

  • [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

思考 && 总结

下篇两个核心知识点:

  1. @wraps(func) 不能省。 装饰器返回的是 wrapper 函数,如果不做信息恢复,原函数的 __name____doc__ 全部丢失。这是生产环境日志和文档的暗坑。
  2. 带参数的装饰器 = 三层嵌套函数。 最外层接收装饰器参数(配置),中间层接收函数,最内层接收调用参数。理解这个结构之后,@app.route('/')@retry(times=3) 的原理就完全通了。

结尾

各位小伙伴,装饰器上下篇到此全部结束。感谢阅读!

源码骑士 --- Python 全栈 & 系统架构

👀 关注:跟博主一起从源码视角深耕底层原理

❤️ 点赞:让优质内容被更多人看见

收藏:核心知识点存好,随用随查

💬 评论:分享你的经验或疑问,一起交流

🔄 一键四连:不要忘记给博主"一键四连"哦!

🗡️ 寄语:技术之路,同行的人会让前路更有方向

结语:装饰器是 Python 的灵魂特性之一,上篇讲闭包,下篇讲应用------两篇读完,面试不怕。一键四连别忘了!

相关推荐
LAM LAB1 小时前
【Web】网页如何模拟移动端获取定位\定位模拟测试
开发语言·前端·javascript
KIO no way1 小时前
AI发布工作流配置指南_用CSDN_AI数字营销把分发自动化
人工智能·python·自动化
小糯米6011 小时前
C语言文件操作
c语言·开发语言·数据结构
caimouse1 小时前
Reactos 第 9 章 设备驱动 — 9.4 内核劳务线程
开发语言·windows
Doker 多克1 小时前
Spring AI Alibaba—快速构建ReactAgent
java·开发语言·前端·ai编程
张忠琳1 小时前
【Go 1.26.4】Golang Slice 深度解析
开发语言·后端·golang
码云骑士2 小时前
09-Python模块导入机制-sys.path与循环导入的死锁式排查
开发语言·python
星恒随风2 小时前
C++ 模板初阶:从泛型编程、函数模板到类模板,一篇打通基础概念
开发语言·c++·笔记·学习
郝学胜-神的一滴2 小时前
Qt 高级开发 031:QListWidget图标布局实战
开发语言·c++·qt·程序人生·软件构建·用户界面