装饰器的引入
在 Python 中,函数和普通的数字、字符串一样,也是一种“对象”。这意味着你可以把函数作为一个参数,传给另一个函数。 示例:
# 1. 定义一个普通函数
def greet():
print("你好,世界!")
# 2. 定义另一个函数,它的参数是一个“函数”
def run_function(func):
print("准备开始执行...")
func() # 在这里执行传进来的函数
print("执行结束。")
# 3. 把 greet 函数像变量一样传进去(注意:传的是名字 greet,不要加括号)
run_function(greet)
运行结果
准备开始执行...
你好,世界!
执行结束。
我们也可以在函数内部定义一个函数
def outer_function():
print("这是外层函数")
def inner_function():
print("这是内层函数")
# 外层函数可以把内层函数当作“产品”返回出去
return inner_function
# 获取返回的内层函数
my_func = outer_function()
my_func() # 执行内层函数
输出结果
这是外层函数
这是内层函数
这里不是闭包,因为内部函数没有使用到外部函数变量。
函数也是一种对象,可以作为参数传给一个函数的形参
# 原有的普通业务函数
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()
运行结果
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!
我们原来有个函数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):是一种允许在不修改原有函数代码的前提下,动态为其添加新功能的语法特性。其本质是一个高阶函数,它接收被装饰的函数作为参数,并返回一个增强后的新函数。
上面的实现就变为:
# 原有的普通业务函数
# 包装器函数:接收旧函数,返回新函数
def log_decorator(func):
def wrapper():
print("[日志]:业务函数开始执行...")
func() # 执行原函数
print("[日志]:业务函数执行完毕!")
return wrapper # 返回包装后的新函数
@log_decorator
def order_food():
print("用户成功下单了一份外卖!")
order_food()
运行结果
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!
跟上面输出一致
有的时候我们原来的函数需要接受变量,变量可能是不可变类型,也有可能是可变类型,也有可能是复杂对象,那就需要使用python中的魔术变量*args, **kwargs
上面又演化为这样:
# 原有的普通业务函数
# 包装器函数:接收旧函数,返回新函数
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")
输出结果
[日志]:业务函数开始执行...
hello
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!
在实际开发中,order_food会被引用,不仅如此,我可能还是order_food.__name__获取函数名去实现自己的功能,但是这个name好像变成内部定义的wrapper名称,这就影响原来代码实现了吗,怎么办呢? 内部函数使用@functools.wraps(func)装饰
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__)
输出结果
[日志]:业务函数开始执行...
hello
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!
order_food
带参数的装饰器
示例:
# 原有的普通业务函数
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不就可以了。
# 原有的普通业务函数
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)不就是
def log_decorator(func):
def wrapper(*args, **kwargs):
print("[日志]:业务函数开始执行...")
func(*args, **kwargs) # 执行原函数
print("[日志]:业务函数执行完毕!")
return wrapper # 返回包装后的新函数
我在只有在inner调用了,那不如直接写在里面,成为outer_fuc一部分。
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()
运行结果
京西外卖
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!
后续我要修改为渴了么平台只要order_food = outer_fuc("渴了么")(order_food)
这个python也有装饰形式,@outer_fuc("渴了么") 装饰要装饰的函数就行,上面就变成:
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()
输出结果:
渴了么
京西外卖
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!
[日志]:业务函数开始执行...
用户成功下单了一份外卖!
[日志]:业务函数执行完毕!
神奇的事情出现了:渴了么与京西外卖先打印出来。就这涉及装饰器的加载问题
装饰器的加载
要彻底搞懂装饰器的加载详情(也叫配置期/导入期),我们需要把 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一下,装饰顺序是怎样的? 示例:
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()
运行结果
我先睡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
此时在内存中,原函数的肉体被包在了最里面:
[ 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先计算完成吗。
评论
0 条