从零推导 Python 装饰器:不用背语法,看完这篇就懂了

前置认知:函数也是个"东西"

在 Python 里,函数和数字、字符串一样,是一个可以传来传去的对象。你可以把函数存进变量、当作参数传给另一个函数、甚至让一个函数返回另一个函数。

这个认知是理解装饰器的地基。如果觉得抽象,可以这样理解:

把函数想象成一道菜谱 。菜谱本身不是菜,它只是一张写满步骤的纸。你可以把这张纸递给别人(传参),也可以用一张新菜谱替换掉旧菜谱(赋值)。装饰器做的事情,就是在不改写原菜谱的前提下,给这道菜加一层摆盘

一旦接受了"函数是个能传来传去的东西",后面的一切都是水到渠成。


第一步:手动包装函数

最朴素的思路:把函数当作参数传进去,在调用前后加上额外逻辑。

python 复制代码
def fun():
    print("我是原函数")

def wrapper1(func):
    print("装饰前置逻辑")
    func()  # 调用原函数
    print("装饰后置逻辑")

wrapper1(fun)

运行输出:

复制代码
装饰前置逻辑
我是原函数
装饰后置逻辑

wrapper1 接收了 fun,先打印前置逻辑,再替你调用 fun(),最后打印后置逻辑。原函数被完整地"包"在了中间。

但这个写法有个问题:fun 还是原来的 fun,每次想用包装版都得手动写 wrapper1(fun)。能不能让 fun 本身就"升级"成包装后的版本?


第二步:函数可以重新赋值

函数名本质上就是一个指向函数对象的变量名,所以可以重新赋值。

python 复制代码
def wrapper2(func):
    print("装饰前置逻辑")
    return func  # 不加括号()不调用,直接返回函数对象本身

fun = wrapper2(fun)  # 将自身作为参数传递,返回并赋值给fun

运行输出:

复制代码
装饰前置逻辑

注意:这里没有输出 "我是原函数"。因为 wrapper2() 没有调用原函数,只是返回了原函数的引用。

赋值之后,fun 变成了 wrapper2() 的返回值------也就是原函数本身。所以执行 fun() 仍然可以调用原函数,没丢。

但这仍然不够好:前置逻辑在赋值时就跑了,而且原函数没被调用。我们想要的是------赋值时不执行任何东西,等真正调用 fun() 时才执行包装逻辑


第三步:返回一个全新的函数

核心技巧:在 wrapper 里面再套一层函数,外层函数直接返回内层函数。内层函数定义时不会执行,只有被调用时才执行。

python 复制代码
def decorator(func):
    def wrapper():
        print("装饰前置逻辑")
        func()  # 调用原函数
        print("装饰后置逻辑")
    return wrapper  # 返回wrapper函数对象,不调用它

fun = decorator(fun)  # 赋值新函数wrapper给fun,此时wrapper内容没被调用
fun()  # 此时fun就是全新的函数了,调用fun()就是调用wrapper()

运行输出:

复制代码
装饰前置逻辑
我是原函数
装饰后置逻辑

这一步要分清两个阶段:

  • 赋值时(只造壳,不执行)fun = decorator(fun) 调用了 decorator(),把原 fun 传进去。decorator 内部定义了 wrapper 但没有调用它,只是把 wrapper 这个函数对象返回出来,赋给了 fun``。此时 wrapper` 里的代码一行都没跑。
  • 调用时(才执行包装逻辑)fun() 此时调用的已经不是原函数了,而是 wrapper()wrapper 执行前置逻辑 → 调用原函数 → 执行后置逻辑。完整的包装效果在这一刻才显现。

外层函数负责"注册"(接收原函数、返回新函数),内层函数负责"干活"(前置 + 原函数 + 后置)。这就是装饰器的标准结构。


三个必须想通的关键问题

问题 1:为什么参数放在最外层?

有人可能会想:能不能把 func 参数放到内层 wrapper 上?试一下:

python 复制代码
def decorator():
    def wrapper(func):
        print("装饰前置逻辑")
        func()
        print("装饰后置逻辑")
        return func
    return wrapper

fun = decorator()
fun(fun)  # 会报错!

fun = decorator() 返回了 wrapper 函数,赋值给 fun。但此时原函数 fun 已经被 wrapper 覆盖而丢失了 ------变量名 fun 现在指向的是 wrapper,不再是原来的那个函数。

调用 fun(fun) 时,左边的 funwrapper(需要一个参数 func),右边的 fun 也是 wrapper(不是原函数)。类型对不上,直接报错。

结论:必须把参数放在最外层,这样原函数在赋值替换之前就被"捕获"进闭包了,不会因为变量名被覆盖而丢失。

问题 2:原函数有参数怎么办?

前面的例子原函数都没有参数。但真实业务函数几乎都有参数,比如 add(a, b)。答案是用 *args, **kwargs------Python 的"万能参数容器",可以接收任意数量、任意形式的参数:

python 复制代码
def log_decorator(func):
    def inner(*args, **kwargs):
        print(f"【日志】准备调用函数 {func.__name__}")
        res = func(*args, **kwargs)  # 把参数传给原函数,接收返回值
        print(f"【日志】函数执行完毕")
        return res  # 必须把原函数返回值返回出去!很容易漏
    return inner

def add(a, b):
    return a + b

add = log_decorator(add)
print(add(3, 5))

运行输出:

csharp 复制代码
【日志】准备调用函数 add
【日志】函数执行完毕
8

⚠️ 易错点return res 这一行非常容易漏。如果原函数有返回值而 inner 没有把它返回出去,调用方拿到的就是 None,bug 非常隐蔽。记住:装饰器是"透明管道",原函数返回什么,你就原样吐回去。

问题 3:装饰器到底解决了什么问题?

本质: 装饰器是利用闭包 + 函数对象赋值,在不修改原函数源代码、不修改原函数调用方式的前提下,给已有函数动态增加额外功能的工具。

它实际解决的痛点:

痛点 说明
重复代码冗余 需要给多个函数加相同附加逻辑(如日志、计时、权限校验),不用每个函数都抄一遍
侵入业务代码 不想改原有业务代码(开闭原则),只在外围包裹附加逻辑,原函数一行不动
职责混杂 功能解耦,职责拆分,各司其职,代码可读性和可维护性大幅提升
无法灵活扩展 可以灵活叠加多个装饰器、按需开关功能,像搭积木一样组合

@ 语法糖:一行代码背后藏着什么

前面我们一直手写 fun = decorator(fun) 这行赋值。Python 觉得这行太常用了,就发明了 @ 符号来替你干这件事。两种写法完全等价:

python 复制代码
def outer(func):
    def inner():
        print("前置")
        func()
        print("后置")
    return inner

# 写法1:原始手写赋值
def hello():
    print("原函数")

hello = outer(hello)   # 这一行就是 @ 替你干的事

# 写法2:@语法糖简写,和上面完全等价
@outer
def hello():
    print("原函数")

@outer 就是一个快捷方式,它在定义完 hello 之后,自动执行 hello = outer(hello)。仅此而已。但就是这个"仅此而已",给新手挖了不少坑。

坑 1:隐藏了赋值替换动作

手写 hello = outer(hello),你能清晰看到变量 hello 被覆盖,指向了返回的 inner。但 @outer 一行搞定,看不到变量替换,大脑很难反应:hello 已经不是原来的函数了。

💡 破解方法 :每次看到 @xxx,脑子里自动翻译成 函数名 = xxx(函数名)

坑 2:多层带参装饰器嵌套混乱

python 复制代码
def log(level):
    def deco(func):
        def inner():
            pass
        return inner
    return deco

@log("INFO")
def test():
    pass

展开原始代码是:

python 复制代码
temp = log("INFO")
test = temp(test)

@log("INFO") 实际上是两步:先执行 log("INFO") 得到一个装饰器,再用这个装饰器去装饰 test。中间临时变量被隐藏,链条断裂,越看越乱。

坑 3:叠加装饰器执行顺序反直觉

python 复制代码
@A
@B
def f():
    pass

展开后是:

python 复制代码
# 先执行内层 B 包裹 f
f = B(f)
# 再用 A 包裹上一步的结果
f = A(f)

真实代码:f = A(B(f))。书写从上到下,执行从下到上。

记忆口诀:装饰从下往上包,执行从外往里穿

坑 4:被装饰函数元信息丢失

装饰之后,add.__name__ 不再是 "add",变成了 "inner"。在调试、日志、文档生成时会出问题。解决:functools.wraps(func),生产代码必加。

python 复制代码
import functools

def log_decorator(func):
    @functools.wraps(func)  # 保留原函数的名字、文档注释
    def inner(*args, **kwargs):
        print("日志")
        return func(*args, **kwargs)
    return inner

@log_decorator
def add(a, b):
    """两数相加"""
    return a + b

print(add.__name__)  # add,而不是 inner
print(add.__doc__)   # 两数相加

functools.wraps 到底干了什么

@functools.wraps(func) 本质上是把原函数的元信息复制到 inner 上。手写一个简化版就能看清楚核心逻辑:

python 复制代码
def my_wraps(func):
    def decorator(wrapper):
        wrapper.__name__ = func.__name__
        wrapper.__doc__ = func.__doc__
        wrapper.__module__ = func.__module__
        wrapper.__qualname__ = func.__qualname__
        wrapper.__annotations__ = func.__annotations__
        return wrapper
    return decorator

💡 上面是简化版,官方 functools.wraps 还会更新 __dict__ 并设置 __wrapped__ 属性,功能更完整。这里为了理解核心原理做了精简。

展开过程:

  1. temp_decorator = my_wraps(func)------my_wraps 的形参 func = 外面收到的原始函数,返回里面的 decorator 函数
  2. inner = temp_decorator(inner)------inner 传递给参数 wrapper,执行后 inner 的元信息被替换成原函数的

装饰器进阶

带参数的装饰器

如果装饰器本身需要传参(比如日志级别),要多套一层函数。

先看错误写法

python 复制代码
def decorator(func, level):  # 两个参数
    @functools.wraps(func)
    def inner(*args, **kwargs):
        print(f"[{level}] 执行函数 {func.__name__}")
        return func(*args, **kwargs)
    return inner

@decorator("INFO")  # 报错!@只会把函数传给第一个形参func,level没人接收
def test():
    pass

@ 语法糖只能自动传一个参数------被装饰的函数 test@decorator("INFO") 会先把 "INFO" 传给第一个参数 funclevel 收不到值,直接报错。

正确写法:在外面再包一层,专门接收装饰器自身的参数:

python 复制代码
import functools

def log(level):
    # 外层接收装饰器参数 level
    def decorator(func):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            print(f"[{level}] 执行函数 {func.__name__}")
            return func(*args, **kwargs)
        return inner
    return decorator

@log("INFO")
def test():
    print("业务逻辑")

test()

运行输出:

bash 复制代码
[INFO] 执行函数 test
业务逻辑

执行流程:

  1. temp_decorator = log("INFO")------level = "INFO" 被闭包保存,temp_decorator 就是内部的 decorator 函数
  2. test = temp_decorator(test)------decorator 的形参 func = 原始 test 函数,返回 inner 赋给 test
  3. 调用 test() → 执行 inner------inner 既能拿到外层的 level,又能拿到上一层的 func,这就是闭包的威力

坑 5:@ 只能自动传一个参数

这就是上面错误写法的原因。@ 语法糖只负责把被装饰函数传给装饰器,如果装饰器还需要额外参数,就必须用"再套一层"的方式让参数在 @ 之前就通过闭包传入。

多个装饰器叠加

python 复制代码
def dec1(func):
    def inner(*args, **kwargs):
        print("dec1 before")
        r = func(*args, **kwargs)
        print("dec1 after")
        return r
    return inner

def dec2(func):
    def inner(*args, **kwargs):
        print("dec2 before")
        r = func(*args, **kwargs)
        print("dec2 after")
        return r
    return inner

@dec1
@dec2
def f():
    print("f run")

f()

运行输出:

erlang 复制代码
dec1 before
dec2 before
f run
dec2 after
dec1 after

展开过程:

python 复制代码
# 先执行内层 dec2 包裹 f
f = dec2(f)
# 再用 dec1 包裹上一步的结果
f = dec1(f)

调用时像剥洋葱:最外层 dec1 先打印 before,进入 dec2 打印 before,执行原函数 f,然后 dec2 打印 after,最后 dec1 打印 after。

完整口诀:装饰从下往上包,执行从外往里穿,返回从里往外穿

补充:类装饰器

除了用函数写装饰器,还可以用类。原理一样:只要类实现了 __call__ 方法,它的实例就能像函数一样被调用。

python 复制代码
class LogDecorator:
    def __init__(self, func):
        self.func = func  # 接收被装饰的函数

    def __call__(self, *args, **kwargs):
        print(f"【日志】准备调用 {self.func.__name__}")
        res = self.func(*args, **kwargs)
        print(f"【日志】执行完毕")
        return res

@LogDecorator
def greet(name):
    print(f"你好,{name}")
    return "done"

greet("小明")

运行输出:

复制代码
【日志】准备调用 greet
你好,小明
【日志】执行完毕

@LogDecorator 等价于 greet = LogDecorator(greet)。调用 greet("小明") 时,Python 自动触发 __call__ 方法。类装饰器的好处是可以保存状态(比如统计调用次数),在需要维护内部状态的场景下比函数装饰器更方便。


实战:两个真实场景

场景一:函数执行计时器

python 复制代码
import time
import functools

def timer(func):
    @functools.wraps(func)
    def inner(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} 耗时: {end - start:.4f}秒")
        return result
    return inner

@timer
def process_data():
    time.sleep(1.5)
    return "处理完成"

process_data()
# 输出: process_data 耗时: 1.5012秒

给任何函数加上 @timer,就能自动打印执行耗时,排查性能瓶颈时非常有用。

场景二:登录权限校验

python 复制代码
import functools

# current_user 来自 Flask-Login,此处省略导入和初始化
# 在真实项目中它是一个全局代理对象,表示当前登录用户

def login_required(func):
    @functools.wraps(func)
    def inner(*args, **kwargs):
        if not current_user.is_authenticated:
            return "请先登录"
        return func(*args, **kwargs)
    return inner

@login_required
def view_profile():
    return "这是你的个人主页"

@login_required
def edit_profile():
    return "编辑你的资料"

⚠️ 上面的 current_user 是 Flask-Login 提供的全局代理对象,直接复制运行会报 NameError。这里为了聚焦装饰器逻辑省略了框架部分,真实项目中由框架自动注入。

多个接口需要登录校验,不用每个函数里都写 if not logged_in,一个 @login_required 搞定。Flask、Django 的权限装饰器就是这个原理。


一句话总结

把整个推导链串起来:

函数是对象 → 可以当参数传 → 可以被赋值替换 → 外层函数接收原函数、内层函数包裹逻辑 → 返回新函数覆盖旧名字 → @ 语法糖替你写这行赋值。


相关推荐
鬼手点金1 小时前
Hermes‑Agent 使用教程
python·ubuntu·powershell·cmd·ai agent·opencode·hermes
Gu Gu Study1 小时前
【agent认知】宏观Agent的基本两层架构(Agent Loop与Agent Harness)
python·架构
从小就看凹凸曼^o^1 小时前
Python基础3 - 流程控制语句:(1)程序结构
python
小陈的进阶之路1 小时前
爬虫三大解析库:lxml, jsonpath, bs4
开发语言·笔记·python
满怀冰雪2 小时前
17-正则化方法:Dropout、权重衰减与过拟合控制
人工智能·python·深度学习·机器学习·paddlepaddle
2603_965148112 小时前
抖音/快手直播选品:用API快速匹配爆款与低价货源
开发语言·python·自动化·api
用户7088769039382 小时前
RAG检索优化实战:从67%到92%,我做了这4步
python
GEOshijie1232 小时前
智能家居品牌做GEO推荐哪家供应商?品类词抢占案例复盘
人工智能·python·智能家居
天天爱吃肉82182 小时前
行业笔记|高压连接器瞬态接触退化通用检测方法论
人工智能·笔记·python·功能测试·学习·汽车