目录
前言
很多人在学装饰器的时候会感到很难,表现为不懂得装饰器的作用以及为什么要使用装饰器,以及各种书中为了附庸风雅而总是使用的“高级词汇”,这总是不经让人想起python思想中的‘Simple is better than complex",那些使用’高级词汇‘的始作俑者是否违背了这一思想呢?所以我撰写此文的目的很简单,也很明确,就是要用简单易懂的话语让python初学者对装饰器有简单的了解并可以在实际场景中运用,注明:该文章面向的对象是python初学者,如果对文章中的任何内容有任何疑问,欢迎随时提问,我会进行解答
什么是装饰器?
俗话说:“工欲善其事,必先利其器”,装饰器指的是定义一个函数,这个函数的功能就是用来装饰其他函数的,也就是说这个函数的作用是用来给其他函数添加额外的功能的,很多人也许会感到很诧异,为函数添加额外功能在函数内部不就是可以完成的吗?为什么要多此一举地创建一个新的函数来增加功能呢?别急,在接下来的内容中会有所介绍
为什么要使用装饰器?
当我们写完一款程序后,我们是不能轻易地修改它的源码的,’一波激起千层浪',当我们的源码没有问题,能够正常运行时,就不要去修改它了,这时为源码增加功能就成了问题,这时候我们就需要运用装饰器,装饰器的使用符合编程中的重要原则 —— 开放封闭原则
开放封闭原则
1.开放:对扩展功能(增加功能)开放,扩展功能的意思是在源代码不作任何改变的情况下,为其增加功能
2.封闭:对修改源代码,修改调用方式封闭
也就是说,我们开发一款程序,该程序应该是可扩展,而不是可修改的,装饰器就在这种苛刻的要求下孕育而生,装饰器可以在不修改被装饰对象的源代码,也不修改调用方式的前提下,给被装饰对象添加功能,当然,这里的添加功能并不是直接添加功能的,而是通过函数替换的方式进行间接添加功能
闭包函数
在学习装饰器之前,我们需要了解闭包函数,它是装饰器的核心,是必须掌握的
闭:函数存在于另一函数中,位于另一函数的局部名称空间,全局是访问不到的,称该函数是封闭的,即闭函数
包:函数内部包括对外层函数局部名称空间名称的引用,我们就称该函数为包含数
闭包函数的运用场景为:当函数内部无法添加新的形参时,而函数内部又需要外部传参时,可以使用闭包函数
下图的f2是一个简单的闭包函数:
def f1(x):
def f2():
print(x)
return f2
f2存在于f1的局部名称空间,这种情况下,f2可以访问到存在于f1局部名称空间内的名称,无需为f2添加新的形参,最后利用函数的可传递性,将f2的内存地址return出去,在函数外可以使用一个变量接着,并且可以进行调用
def f1(x):
def f2():
print(x)
return f2 # 利用函数的可传递性
res = f1(10)
res()
很多人会疑惑这样做的意义是什么?这不又是多此一举吗?别急,在接下来的实践中,你会发现闭包函数对于装饰器的重要作用
第一个装饰器
接下来的篇章中,我们会进行实践,逐步做出我们的第一个装饰器
从目标出发,目标:为函数inside添加打印运行时间的功能
def inside(group, s):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
方案一
import time
def inside(group, s):
start = time.time()
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
end = time.time()
print(end - start)
直接修改函数,虽然没有改变函数的调用方式,但修改了函数的源代码,违反了开放封闭原则,在项目开发的过程中是不可取的,可能导致整个项目的崩溃,故该方案舍去
方案二
import time
def inside(group, s):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
start = time.time()
inside('红', 5)
end = time.time()
print(end - start)
在全局进行操作,虽然没有修改源代码,也没有修改调用方式,同时为其添加了功能,但你需要想想,每次调用该函数都需要对全局进行操作,这是很不专业的,费时又费力,而且会出现代码冗余现象,故该方案舍去
方案三
def inside(group, s):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
def wrapper():
start = time.time()
inside('红', 5)
end = time.time()
print(end - start)
wrapper()
通过定义函数的方式解决,这其实也就是装饰器的雏形
优点:该方案解决了代码冗余现象,也没有修改源代码,还为其增加了功能
缺点:调用方式被改变了;只能为inside服务,不具有泛用性;当原函数参数改变时,wrapper内部也需要进行对于inside函数内部参数的修改,不利于原函数的修改
方案四
先不考虑调用方式被修改的问题,对于不利于原函数修改的问题进行改进,我们可以运用可变长参数*args和**kwargs进行解决,实现如下:
def inside(group, s, z):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print(f'{z}全军出击')
def wrapper(*args, **kwargs):
start = time.time()
inside(*args, **kwargs)
end = time.time()
print(end - start)
wrapper('蓝', 3, '炮车')
对wrapper函数进行参数传递后,wrapper函数会原封不动的将参数传给inside函数,当inside函数参数修改过后,仅需要对wrapper函数进行同样形式的传递即可
该方案解决了不利于原函数的修改问题,但只能装饰inside,不具有泛用性
方案五
为了解决wrapper函数泛用性问题,我们需要参数function,可是wrapper会将参数原封不动地传递给被装饰对象,我们无法给wrapper增添参数,这时候我们回忆一下闭包函数的运用场景----当函数内部无法添加新的形参时,而函数内部又需要外部传参时,可以使用闭包函数。该情况正好符合闭包函数的运用场景,故我们可以使用闭包函数来解决wrapper函数的泛用性问题,实现如下:
import time
def inside(group, s):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
def outer(function):
def wrapper(*args, **kwargs):
start = time.time()
function(*args, **kwargs)
end = time.time()
print(end - start)
return wrapper
res = outer(inside)
res('蓝', 3)
至此,我们已经写出了一个装饰器outer,它的功能是为被装饰对象增添统计运行时间,通过调用outer,我们既保存了原函数的功能,又增加了新的功能,相信你已经理解了我上文说过的话----装饰器添加功能并不是直接添加功能的,而是通过函数替换的方式进行间接添加功能
接下来我们就需要去解决调用方式改变的问题
方案六
解决该问题之前,我们需要去思考一个问题----是否能够在不改变函数的调用方式和满足开放封闭原则的前提下为函数增添功能?很明显答案是否定的,因为满足开放封闭条件意味着不修改原函数的代码,而不改变函数的调用方式意味着调用的是原函数,而不改变原函数的调用且不修改原函数的代码,怎么可能能为原函数增加功能呢?所以我们只能达到形式上的不改变函数的调用方式,实现如下:
import time
def inside(group, s):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
def outer(function):
def wrapper(*args, **kwargs):
start = time.time()
function(*args, **kwargs)
end = time.time()
print(end - start)
return wrapper
inside = outer(inside)
inside('蓝', 3)
既然在调用装饰器时,返回的是wrapper的内存地址,后通过创建变量,使我们能够在全局调用wrapper,在这个过程中,创建的变量的名称是由我们决定的,那么我们是不是可以通过使变量的名称与被装饰对象的名称一致来达到形式上的调用方式不改变,我们可以称这一步为‘偷梁换柱’
至此,调用方式改变的问题也被我们解决,接下来要做的都是一些优化工作
优化
我们已经基本解决了实现目标过程中出现的问题,想一下,有哪些地方可以优化呢?
1.‘偷梁换柱’操作麻烦
2.原函数有返回值怎么办?
3.怎样实现完美伪装?
语法糖
每次调用装饰器前,我们都需要进行一次‘偷梁换柱’,会导致代码冗杂问题,语法糖的出现就是为了解决‘偷梁换柱’操作麻烦而孕育的,为什么要叫它语法糖?因为它能够让代码更加简洁,并且让我们用起来也很爽,就像吃了糖一样,这是我个人的理解,不能保证解释的正确性,但我们也不必纠结于名字的含义,只需知道怎么用就好了,还是以上面的代码为例,我们可以这样写:
import time
def outer(function):
def wrapper(*args, **kwargs):
start = time.time()
res = function(*args, **kwargs)
end = time.time()
print(end - start)
return res
return wrapper
@outer # 相当于inside = outer(inside)
def inside(group, s):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
把 @装饰器的名字 写在被装饰对象的头顶上(注意:装饰器需要在其之前定义),在以上的例子中,@outer就相当于inside = outer(inside),也就是‘偷梁换柱’操作,自此,语法糖讲解完毕
基本伪装
基本伪装的对象包括 参数,返回值和调用方式,其中,参数,调用方式我们已经伪装得很好了,那么返回值我们该怎么伪装呢?其实很简单,在装饰器内部返回与原函数内部一样的值即可,实现如下:
import time
def outer(function):
def wrapper(*args, **kwargs):
start = time.time()
res = function(*args, **kwargs)
end = time.time()
print(end - start)
return res
return wrapper
@outer
def inside(group, s):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
return 100
完美伪装
回到装饰器的作用----用来给其他函数添加额外的功能,既然装饰器是用来为原函数增加功能的,那么就需要把装饰器和被装饰器伪装得一样,因为‘增加’是在‘原有’的基础上进行的,又因为项目中可能出现对于原函数各种属性的操作,这需要你自己去理解
前面的内容中,我们已经实现了基本伪装,为了实现完美伪装,我们需要对原函数的各种属性进行赋值给装饰器,为此,python为我们提供了一个便捷操作,还是以上面为例,实现如下:
from functools import wraps
import time
def outer(function):
@wraps(function)
def wrapper(*args, **kwargs):
start = time.time()
res = function(*args, **kwargs)
end = time.time()
print(end - start)
return res
return wrapper
@outer
def inside(group, s):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
return 100
上图的wraps是一个有参装饰器,在后续的内容中会讲解到,它的作用是将function内的各种属性赋值给被装饰对象,即wrapper
这样,我们就完成了完美伪装
有参装饰器
import time
def outer(function):
def wrapper(*args, **kwargs):
start = time.time()
res = function(*args, **kwargs)
end = time.time()
print(end - start)
return res
return wrapper
@outer
def inside(group, s):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
return 100
还是从目标出发,目标:为outer内部添加参数name,并在内部打印
那么我们是否能够直接为outer添加参数name呢?
如果你前面的内容掌握的足够好的话,那么你的答案肯定是不能,因为受语法糖的限制,很多人会诧异,为什么受语法糖的限制?那你好好想一想语法糖的本质是什么?在程序运行到@outer时,其实质是 inside = outer(inside) ,直接为outer添加参数的话,会导致其缺少参数name而导致报错,那么我们能否可以将name定义成默认参数呢?可以是可以,但将name定义成默认形参这样做的意义又是什么呢?你仔细想一想
那么我们应该如何做呢?那你再联想一下前面学的内容,是否能激起你记忆的回波呢?我们可以用闭包函数啊,实现如下:
import time
def g_outer(name):
def outer(function):
def wrapper(*args, **kwargs):
print(name)
start = time.time()
function(*args, **kwargs)
end = time.time()
print(end - start)
return wrapper
return outer
@g_outer('yuanweihua')
def inside(group, s):
print('欢迎来到王者荣耀')
print(f'你出生在{group}方阵营')
print(f'敌军还有{s}秒到达战场')
time.sleep(s)
print('全军出击')
return 100
其中,@g_outer('yuanweihua') 等效于@outer ,因为在g_outer中返回了outer,并且解决了上面的增添参数问题
根据上面的内容,我们也可以得出结论,装饰器至多三层函数,内层加功能,中层传原函数,外层传新参
装饰器叠加
这是最后一个知识点了,加油啊
def outer3(function):
def wrapper3(*args, **kwargs):
print('outer3 start')
function(*args, **kwargs)
print('outer3 finish')
return wrapper3
def outer2(function):
def wrapper2(*args, **kwargs):
print('outer2 start')
function(*args, **kwargs)
print('outer2 finish')
return wrapper2
def outer1(function):
def wrapper1(*args, **kwargs):
print('outer1 start')
function(*args, **kwargs)
print('outer1 finish')
return wrapper1
@outer3 # home = outer3(home) home--->wrapper2
@outer2 # home = outer2(home) home--->wrapper1
@outer1 # home = outer1(home) home--->home()
def home():
print('this is home')
home()
如上图所示,当装饰器叠加使用时,它的加载顺序为 @outer1--->@outer2--->@outer3
该代码的运行结果为
outer3 start
outer2 start
outer1 start
this is home
outer1 finish
outer2 finish
outer3 finish
为什么会出现这样的结果呢?让我们来对代码进行分析:
首先会执行@outer1,即home = outer1(home) ,后执行@outer2,即home = outer2(home),最后执行@outer3,即 home = outer3(home) ,所以在全局调用的home(),其实是outer3(home)的返回值,即wrapper3,再执行wrapper3,打印outer3 start,而wrapper3中的function是给outer3传的参数,即outer2(home)的返回值wrapper2,再执行wrapper2······以此类推,最后执行原home,后继续执行wrapper1,打印outer1 finish,后回到wrapper2,打印outer2 finish······以此类推
至此,装饰器篇完结
总结
这是我在csdn的第一篇文章,我也知道这可能没什么流量,但我不知道我为什么要花费那么多的时间来创作一篇没有几个人会看到的文章,可能是因为不想让一些有热爱的人因为没有好的教导,凭着一知半解的知识而灰心吧,在我学习python的过程中,我也曾因为没有好的教导而迷茫过,但屏幕前的你们可别气馁啊!