装饰器的引入

在 Python 中,函数和普通的数字、字符串一样,也是一种“对象”。这意味着你可以把函数作为一个参数,传给另一个函数。 示例:

python
# 1. 定义一个普通函数
def greet():
    print("你好,世界!")

# 2. 定义另一个函数,它的参数是一个“函数”
def run_function(func):
    print("准备开始执行...")
    func()  # 在这里执行传进来的函数
    print("执行结束。")

# 3. 把 greet 函数像变量一样传进去(注意:传的是名字 greet,不要加括号)
run_function(greet)

运行结果

text
准备开始执行...
你好,世界!
执行结束。

我们也可以在函数内部定义一个函数

python
def outer_function():
    print("这是外层函数")

    def inner_function():
        print("这是内层函数")

    # 外层函数可以把内层函数当作“产品”返回出去
    return inner_function

# 获取返回的内层函数
my_func = outer_function() 
my_func()  # 执行内层函数

输出结果

text
这是外层函数
这是内层函数

这里不是闭包,因为内部函数没有使用到外部函数变量。

函数也是一种对象,可以作为参数传给一个函数的形参

python
# 原有的普通业务函数
def order_food():
    print("用户成功下单了一份外卖!")

# 包装器函数:接收旧函数,返回新函数
def log_decorator(func):
    def wrapper():
        print("[日志]:业务函数开始执行...")
        func()  # 执行原函数
        print("[日志]:业务函数执行完毕!")
    return wrapper  # 返回包装后的新函数

# 手动进行包装
order_food = log_decorator(order_food)

# 此时调用 order_food,它已经不是原来的函数了,而是被包装过的 wrapper
order_food()

运行结果

text
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!

我们原来有个函数order_food,执行order_food()只会打印:"用户成功下单了一份外卖"。后来进行了一系列操作,执行order_food()后会打印日志信息,只有后面调用都会打印,那么我们做了什么操作呢?

  • 定义了一个新函数log_decorator,接受一个函数对象。
  • 我自己再定义了一个内部函数,这个函数内部就会调用传进来的函数,同时添加我自己的动作

这个函数从外部看:给我个函数对象,我会调用你,同时我也会添油加醋哦,同时给你一个新的函数对象。 分析: order_food = log_decorator(order_food)

  • 先进行右边:order_food变量指向的函数对象传给了log_decorator,log_decorator内部定义了一个内部函数,并且引用了外部函数的变量func,并且返回内部函数,这就出发闭包条件,传进来的func记录在了内部函数对象的__closure__中
  • log_decorator返回了者个新的函数对象
  • order_food重新指向返回的函数对象,指向地址发生了变化, order_food就不是原来的 order_food了,但原来的函数对象保存在了新order_food的__closure__中

order_food()执行时调用wrapper函数实现逻辑,func() 函数从__closure__中获取。

为何还使用order_food这个名称,如果赋值给其他变量完全可以的,逻辑也一样,但是我们的order_food函数是原有的,可能在一个项目中会在其他地方引用,经过闭包处理仍然可以正常引用,只不过功能改变了。

到这里我们就实现了不改变原来函数,同时添加了新功能。但是如果每次都这样写是不是很麻烦啊,python就引入一个@就函数,这样处理后总得有个名字吧,总不能说@某函数,于是就有了一个名称->"装饰器"。有了名称之后总得定义下是什么吧,不然只听到名称,不知道啥意思,于是就定义: 装饰器(Decorator):是一种允许在不修改原有函数代码的前提下,动态为其添加新功能的语法特性。其本质是一个高阶函数,它接收被装饰的函数作为参数,并返回一个增强后的新函数。

上面的实现就变为:

python
# 原有的普通业务函数


# 包装器函数:接收旧函数,返回新函数
def log_decorator(func):
    def wrapper():
        print("[日志]:业务函数开始执行...")
        func()  # 执行原函数
        print("[日志]:业务函数执行完毕!")

    return wrapper  # 返回包装后的新函数


@log_decorator
def order_food():
    print("用户成功下单了一份外卖!")


order_food()

运行结果

text
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!

跟上面输出一致

有的时候我们原来的函数需要接受变量,变量可能是不可变类型,也有可能是可变类型,也有可能是复杂对象,那就需要使用python中的魔术变量*args, **kwargs

上面又演化为这样:

python
# 原有的普通业务函数


# 包装器函数:接收旧函数,返回新函数
def log_decorator(func):
    def wrapper(*args, **kwargs):
        print("[日志]:业务函数开始执行...")
        func(*args, **kwargs)  # 执行原函数
        print("[日志]:业务函数执行完毕!")

    return wrapper  # 返回包装后的新函数


@log_decorator
def order_food(msg):
    print(msg)
    print("用户成功下单了一份外卖!")


order_food("hello")

输出结果

text
[日志]:业务函数开始执行...
hello
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!

在实际开发中,order_food会被引用,不仅如此,我可能还是order_food.__name__获取函数名去实现自己的功能,但是这个name好像变成内部定义的wrapper名称,这就影响原来代码实现了吗,怎么办呢? 内部函数使用@functools.wraps(func)装饰

python
from functools import wraps


# 包装器函数:接收旧函数,返回新函数
def log_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("[日志]:业务函数开始执行...")
        func(*args, **kwargs)  # 执行原函数
        print("[日志]:业务函数执行完毕!")

    return wrapper  # 返回包装后的新函数


@log_decorator
def order_food(msg):
    print(msg)
    print("用户成功下单了一份外卖!")


order_food("hello")

print(order_food.__name__)

输出结果

text
[日志]:业务函数开始执行...
hello
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!
order_food

带参数的装饰器

示例:

python
# 原有的普通业务函数
def order_food(*args,**kwargs):
    print("用户成功下单了一份外卖!")

# 包装器函数:接收旧函数,返回新函数
def log_decorator(func):
    def wrapper(*args,**kwargs):
        print("[日志]:业务函数开始执行...")
        func(*args,**kwargs)  # 执行原函数
        print("[日志]:业务函数执行完毕!")
    return wrapper  # 返回包装后的新函数

# 手动进行包装
order_food = log_decorator(order_food)

# 此时调用 order_food,它已经不是原来的函数了,而是被包装过的 wrapper
order_food()

在这个基础上,老板有了新的要需求:我要打印外卖是那个平台信息

于是你就order_food("京西外卖")

可是在我们不动order_food原来实现下怎么在wrapper里获取到呢? *args,**kwargs里你有没有名称指引,都放在两个变量内存块里。而且这个变量可能会有其他变量。总的变量名来接受引用吧。你可能说args(0)可以拿到,这没错,可是这写死了呀,万一代码传参时没有写在最前面呢。虽然很拙劣地实现了,但是感觉硬凑。

我们联想到函数内部调用外部func,是不是可以借用这种思路? order_food = log_decorator(order_food) 这里我们确认只要执行 log_decorator就会装饰order_food,log_decorator记录了实现方法。我是不是可以再定义一个函数实现自己的功能,我里面用log_decorator去实现order_food不就可以了。

python
# 原有的普通业务函数
def order_food(*args, **kwargs):
    print("用户成功下单了一份外卖!")


# 包装器函数:接收旧函数,返回新函数


def log_decorator(func):
    def wrapper(*args, **kwargs):
        print("[日志]:业务函数开始执行...")
        func(*args, **kwargs)  # 执行原函数
        print("[日志]:业务函数执行完毕!")

    return wrapper  # 返回包装后的新函数


def outer_fuc(platform):
    print(platform)

    def inner(f):
        return log_decorator(f)
    return inner


tmp = outer_fuc("京西外卖")
order_food = tmp(order_food)
order_food()

outer_fuc表示:

  • 我可以接受一个变量,我会返回一个可以生成装饰器的对象,只要你传递函数进去就可以了。这样我就可以任意实现我自己的 outer_fuc,只要返回生成装饰器的对象就可以。 我们在整合下代码log_decorator(f)不就是
text
def log_decorator(func):
    def wrapper(*args, **kwargs):
        print("[日志]:业务函数开始执行...")
        func(*args, **kwargs)  # 执行原函数
        print("[日志]:业务函数执行完毕!")

    return wrapper  # 返回包装后的新函数

我在只有在inner调用了,那不如直接写在里面,成为outer_fuc一部分。

python
def order_food(*args, **kwargs):
    print("用户成功下单了一份外卖!")


def outer_fuc(platform):
    print(platform)
    def inner(f):
        def wrapper(*args, **kwargs):
            print("[日志]:业务函数开始执行...")
            f(*args, **kwargs)  # 执行原函数
            print("[日志]:业务函数执行完毕!")

        return wrapper  # 返回包装后的新函数
    return inner


order_food = outer_fuc("京西外卖")(order_food)
order_food()

运行结果

text
京西外卖
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!

后续我要修改为渴了么平台只要order_food = outer_fuc("渴了么")(order_food)

这个python也有装饰形式,@outer_fuc("渴了么") 装饰要装饰的函数就行,上面就变成:

python
def outer_fuc(platform):
    print(platform)
    def inner(f):
        def wrapper(*args, **kwargs):
            print("[日志]:业务函数开始执行...")
            f(*args, **kwargs)  # 执行原函数
            print("[日志]:业务函数执行完毕!")

        return wrapper  # 返回包装后的新函数
    return inner

@outer_fuc("渴了么")
def order_food_elm(*args, **kwargs):
    print("用户成功下单了一份外卖!")

@outer_fuc("京西外卖")
def order_food_jx(*args, **kwargs):
    print("用户成功下单了一份外卖!")

order_food_elm()
order_food_jx()

输出结果:

text
渴了么
京西外卖
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!

神奇的事情出现了:渴了么与京西外卖先打印出来。就这涉及装饰器的加载问题

装饰器的加载

要彻底搞懂装饰器的加载详情(也叫配置期/导入期),我们需要把 Python 解释器当成一个“打工人”,看看它在从上到下读取代码时,底层究竟发生了什么。

  • 核心结论:加载阶段的本质是什么? 装饰器的加载,本质上是 “函数的重新组装与赋值”。当 Python 解释器看到 @ 语法糖时,它会暂停向下读取,立刻在后台执行一次函数调用,把你的原函数“套上外壳”,变成一个全新的函数。

  • 步骤拆解: 假设解释器读到了这样一段标准的带参数装饰器代码:

python
@outer_fuc("渴了么")
def order_food():
    pass

在加载阶段,解释器的内心戏和底层动作分为以下 3 个微步骤:

  • 提取参数,触发最外层(Outer)
    • 解释器的动作:看到 @outer_fuc("渴了么") 后面带有括号和参数。
    • 底层执行:立刻调用 outer_fuc("渴了么")。
    • 内存变化:
      • 创建了当前函数的局部作用域,变量 platform = "渴了么" 被锁死在闭包里。
      • 此时,如果你在最外层写了 print(platform),它会立刻喷出结果。
    • 返回值:这个步骤执行完后,会返回中间层的 inner 函数。
  • 捕捉原函数,触发中间层(Inner)
    • 解释器的动作:拿到了上一步返回的 inner,同时看到了下面紧跟着的 def order_food()。
    • 底层执行:立刻调用 inner(order_food)(把原函数当成参数传进去)。
    • 内存变化:原函数 order_food 的内存地址被锁死在闭包的变量 f 中。
    • 返回值:这个步骤执行完后,会返回最内层的 wrapper 函数。
  • 偷梁换柱(关键赋值)
    • 解释器的动作:拿到了最终的 wrapper 函数。
    • 底层执行:在当前命名空间中,将原本的函数名 order_food 指向新返回的 wrapper。
    • 等价于:order_food = wrapper

为什么必须要“立即执行”?

  • 静态语法的需要(内存安全)Python 是一门解释型语言,但它也有编译期(生成字节码)。当代码加载完毕后,Python 必须保证内存里所有的变量名和函数名都是确定且可用的。如果不在加载阶段把 order_food 替换成 wrapper,那么程序运行到一半时,系统就无法知道 order_food 到底是被包装过的函数,还是原本的函数。
  • 一次元空间开销优化(提升性能)加载阶段:外层的参数传递、闭包环境的搭建,在整个程序生命周期里只执行一次。运行阶段:因为加载阶段已经把复杂的“套娃”工作做完了,后续不论你调用 order_food() 1万次还是10万次,Python 都只需要直接一头扎进 wrapper 里执行业务即可,不需要每次都重新去解析参数、重新包装。这极大地提升了运行效率。

这个比较好理解,反正装饰器要调用原函数最终生成新函数,我不如在编译阶段把你要装饰的原函数封装成新函数,你要用时直接调用我,我有原函数内存记录。

多层装饰器

奇怪的老板又来了,最近外卖太多,赚钱太多,要求慢一点,懒惰的你直接写了一个装饰器sleep一下,你有个疑问了,我到底先打印日志还是sleep一下,装饰顺序是怎样的? 示例:

python
import time


def outer_fuc(platform):
    print(platform)

    def inner(f):
        def wrapper(*args, **kwargs):
            print("[日志]:业务函数开始执行...")
            f(*args, **kwargs)  # 执行原函数
            print("[日志]:业务函数执行完毕!")

        return wrapper  # 返回包装后的新函数

    return inner


def business_sleep(seconds):
    print(f"我先睡{seconds}秒啊....")
    time.sleep(seconds)

    def inner(f):
        def wrapper(*args, **kwargs):
            print("我睡好了,开始搞业务了......")
            f(*args, **kwargs)
            print("业务搞完了......")
        return wrapper

    return inner



@business_sleep(5)
@outer_fuc("京西外卖")
def order_food_jx(*args, **kwargs):
    print("用户成功下单了一份外卖!")


order_food_jx()

运行结果

text
我先睡5秒啊....
京西外卖
我睡好了,开始搞业务了......
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!
业务搞完了......
  • 这里先打印"我先睡5秒啊....",然后打印了"京西外卖",这就触发了加载机制。说明底层会先加载business_sleep,再加载outer_fuc。
  • 接着打印"我睡好了,开始搞业务了......",business_sleep的wrapper执行了,f还没实现。
  • 接着打印"[日志]:业务函数开始执行...",outer_fuc的wrapper执行了,f还没实现。
  • 接着打印"用户成功下单了一份外卖!",outer_fuc的wrapper执行了,f实现了
  • 接着打印"[日志]:业务函数执行完毕!",outer_fuc的wrapper执行了,率先执行完
  • 最后打印"业务搞完了......",business_sleep执行完了

结果上看,先从business_sleep执行,outer_fuc先完成,最后business_sleep。似乎有"先从上面执行,从下往上逐一完成"的规律。根据上面讲解的装饰器的加载原理,最终调用order_food_jx函数时只会调用最终组装好的函数功能,所以重点就是编译期多装饰器是怎么组装的。

加载过程

加载顺序是从上往下,打印结果也证明了。 当 Python 解释器一启动,扫描到 order_food_jx 函数头顶上的两个 @ 语法糖:

  • 首先扫描到 @business_sleep(5):
    • 解释器立刻调用 business_sleep(5)。 加载阶段也真的会sleep,一般是写在wrapper里的,注意此时不是读代码,是真的执行,只有执行才能创建innner并拿到inner内存
    • 控制台瞬间喷出:我先睡5秒啊....(此时程序真的卡住睡了 5 秒)。
    • 执行完毕后,返回了 business_sleep 的 inner 函数(我们暂且称它为 sleep_inner)。
  • 紧接着扫描到 @outer_fuc("京西外卖"):
    • 解释器立刻调用 outer_fuc("京西外卖")。
    • 控制台瞬间喷出:京西外卖。
    • 执行完毕后,返回了 outer_fuc 的 inner 函数(我们暂且称它为 jx_inner)。

关键的“套娃”组装发生了: 在加载的最后一步,Python 开始把这些返回的 inner 函数像套娃一样组装起来。

  • 底层先执行:jx_wrapper = jx_inner(原函数 order_food_jx)
  • 底层再执行:sleep_wrapper = sleep_inner(jx_wrapper)
  • 最后赋值覆盖:order_food_jx = sleep_wrapper

此时在内存中,原函数的肉体被包在了最里面:

text
[ sleep_wrapper 壳 [ jx_wrapper 壳 [ 真正的原函数 order_food_jx ] ] ]
调用过程

当你在代码最后一行加上括号 order_food_jx() 真正调用它时,程序开始像剥洋葱一样,从最外层的壳一路剥到最里面的原函数:

  • 外层的壳一路剥到最里面的原函数:进入最外壳(sleep_wrapper):
    • 打印:我睡好了,开始搞业务了......
    • 在内部准备执行它的原函数(也就是代码里的 f(...)),但此时的 f 其实是中层的 jx_wrapper。
  • 进入中层壳(jx_wrapper):
    • 打印:[日志]:业务函数开始执行...执行核心业务(此时的 f 才是真正的最内层业务代码)。
    • 执行核心业务:
      • 打印:用户成功下单了一份外卖!
  • 业务结束,开始向外层返回(弹栈):
    • 中层壳剩下的代码执行,打印:[日志]:业务函数执行完毕!
    • 最外壳剩下的代码执行,打印:业务搞完了......

看起来很像栈操作,其实就是递归调用了,最外层调用wrapper时碰到f函数就去执行f函数,发现f函数又有调用另外一个wrapper,以此类推,直到最后一个f函数完成,递归返回继续操作,这个就是"套娃" 组装形成的递归。

其实就是从上往下执行,递归调用的原因形成下面的先完成了。我们在用递归实现斐波那契数列不就是1+1先计算完成吗。