一、开头说两句
大家好,我叫黎潘,很高兴有机会给大家分享一些我的学习感受,作为一名零基础转行刚一年的测试新手来说,深知自己在技术经验方面落后太多,难免会有急于求成的心态,这也就导致自己在学习新知识时似懂非懂,刚开始学完那会还胸有成竹,一段时间之后却又忘的一干二净,导致我要不停回去复习,还始终不得要领,难以在实践中灵活运用。
相信有不少同学跟我一样徘徊踌躇,现在老师给予了我一个给大家分享经验的机会,我也刚好结合前段时间复习关于Python装饰器的理解来说下,若有不对的地方,还望各位同学,同行,老师及时指出。
二、装饰器必知基础
其实很多知识点没有牢牢掌握,是因为最最基础的知识没有理解透彻导致。这也是我在学习装饰器时对于自己的评价,所以先让我们来聊聊学习装饰器所需要的基础知识。
1、形参与实参
函数的参数分为形式参数和实际参数,简称形参和实参。
- 形参即在定义函数时,括号内声明的参数。形参本质就是一个变量,用来接收外部传来的值
- 实参即在调用函数时,括号内传入的值,值可以是常量,变量,表达式或三者的组合
具体使用时又分为位置参数,关键字参数和默认参数
代码语言:javascript复制def info(name,age,sex='male')
print(f'name:{name} age:{age} sex:{sex}')
info(name='jack',18)
上述示例中,调用函数时以key=value形式的就是关键字参数,定义函数时name,age为位置参数,sex为默认参数。
注意:
- 调用函数时,实参可以是按位置或关键字的混合使用,但必须保证关键字参数在位置参数后面,且不可以对一个形参重复赋值
- 默认参数的值通常应设为不可变类型
2、可变长度参数*args和**kwargs
参数的长度可变指的是调用函数时,实参的个数可以不固定,而在调用阶段,实参无非是按照位置或者按关键字两种形式,因此就出现了两种解决方案来处理。
2.1 可变长度的位置参数
如果在最后一个形参名前加*号,那么在调用函数时,溢出的位置实参都会被接受,以元组的形式保存下来赋值给该形参。
代码语言:javascript复制def func(x,y,z=1,*args):
print(x,y,z,args)
func(1,2,3,4,5,6,7)
>>1 2 3 (4,5,6,7)
#这里起作用的就是*号,相当于溢出的位置参数赋值给了它后面的变量,即args=(4,5,6,7)
2.2 可变长度的关键字参数
如果在最后一个形参名前加**号,那么在调用函数时,溢出的关键字参数,会以字典的形式保存下来赋值给形参。
代码语言:javascript复制def func(x,**kwargs):
print(x)
print(kwargs)
func(x=1,y=2,z=3)
>>1
>>{'y':2,'z':3}
#同上此时相当于把溢出的关键字实参一,y,z都被**接收以字典的形式赋值给kwargs,即kwargs={'y':2,'z':3}
2.3 组合使用
可变参数*args与关键字参数kwargs通常是组合在一起使用的,如果一个函数的形参为上述两种类型,那么代表该函数可以接收任何形式,任意长度的参数。
代码语言:javascript复制def wrapper(*args,**kwargs):
pass
在该函数内部还可以把接受到的实参传给另一个函数,这在后面推导装饰器时大有用处。
代码语言:javascript复制def func(x,y,z):
print(x,y,z)
def wrapper(*args,**kwargs):
func(*args,**kwargs)
wrapper(1,y=2,z=3)
>>1 2 3
分析:
此处在给wrapper传参时,其遵循的事函数func的参数规则,第一步,位置参数1被接受,以元组形式保存下来赋值给args,即args=(1,),关键字参数y=2,z=3被**以字典形式接收赋值给kwargs,即kwargs={'y':2,'z':3};第二步,执行func(args,kwargs),即func((1,),{'y':2,'z':3}),等同于func(1,y=2,z=3)。
3、函数对象和闭包
函数对象指的是函数可以被当做"数据"来处理,具体可以分为四个方面的使用
3.1 函数可以被引用
代码语言:javascript复制def add(x,y):
return x y
func = add
func(1,2)
>>3
3.2 函数可以作为容器类型的元素
代码语言:javascript复制dic = {'add':add}
>>dic
>>{'add': <function add at 0x100661e18>}
>>dic['add'](1,2)
>>3
3.3 函数可以作为参数传入另一个函数
代码语言:javascript复制def foo(x,y,func):
return fun(x,y)
>>foo(1,2,add)
>>3
3.4 函数的返回值可以是一个函数
代码语言:javascript复制def bdd():
return add
func=bdd()
func(1,2)
>>3
3.5 闭包函数有两个关键点
"闭":值得时函数定义在另一个函数内即内嵌函数。
"包":指的是该函数包含对外层函数作用于变量的引用。
代码语言:javascript复制def f1():
x = 1
def f2():
print(x)
f2()
#此时f2就是内嵌函数,为‘闭’,f2有对外层变量x的引用,为‘包’
#但是我们不想在内部调用f2函数该怎么办呢
#这个时候函数对象的引用,可以作为返回对象就可以解决,即:
def f1():
x = 1
def f2():
print(x)
return f2 #注意不能加括号,否者就是返回f2的执行结果,我们需要的是他的内存地址以供在外部可以随时调用
f = f1() #此刻变量f接受到的就是f2的内存地址
总结:
闭包函数提供了一种新的为函数体传参的方式,为了给f2传值,在他的同级作用域给了他一个值,f2在整体缩进,外层再给他嵌套一个函数f1包起来。此时f1从原来的全局变成了局部,为了使我们在全局依然可以调用它,通过return函数对象再返回到全局。
三、什么是装饰器
上边讲了这么多,可能大家有点疑惑怎么还不介绍装饰器。不用急,这也是我们在学习中常犯的错误,急于求成反而不利于对知识的吸收好消化理解。其实在潜移默化中,我们已经把大部分构成装饰器的基本知识提到了,只是还未进行归纳整理。下面我们又将重新一步一步推导它的由来。
定义:定义一个函数(类),该函数专门用来为其他函数(对象)添加额外的功能。
装饰器本质上是一个python函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志,性能测试,事务处理,缓存,权限校验等场景。
四、为什么用装饰器
我们在为一个对象添加新功能时,往往秉持着开放封闭原则。
- 开放:指的是对拓展功能是开放的
- 封闭:指的是修改源代码是封闭的
即在不修改被装饰对象源代码和调用方式的情况下为被装饰对象新增功能。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。
五、装饰器的推导
提出需求:为index函数新增计算代码运行时间的功能,必须符合开放封闭原则。
代码语言:javascript复制import time
def index(x,y):
time.sleep(2)
print(f'来自index的{x}和{y}')
1、方案一
代码语言:javascript复制def index(x, y):
start = time.time()
time.sleep(2)
print(f"来自index的{x}和{y}")
stop = time.time()
print(stop-start)
结果:虽然实现了功能,但破环了开放封闭原则,修改了源代码,不符合要求,失败。
2、方案二
代码语言:javascript复制start = time.time()
index(1, 2)
stop = time.time()
print(stop-start)
结果:上述代码虽然没有修改源代码,也实现了功能,但是每次使用都要加上这三行代码,太过冗余,失败。
3、方案三
代码语言:javascript复制'''在方案二的基础上进行优化,为了解决代码冗余,我们把它写成一个函数'''
def wrapper():
start = time.time()
index(1, 2)
stop = time.time()
print(stop-start)
wrapper()
结果:此时我们不用每次加上三行代码,只需调用wrapper函数即可,但是复用性依然不够,可以在进行优化。
4、方案四
优化:解决index的传参被写死了的问题
代码语言:javascript复制#此时函数参数的知识就用上了
def wrapper(a, b):
start = time.time()
index(a, b)
stop = time.time()
print(stop-start)
wrapper(1,2)
#但是可能在后续的需求中index的传参个数会发生变化
#此时可变长度参数就能帮上大忙了
def wrapper(*args, **kwargs):
start = time.time()
index(*args, **kwargs)
stop = time.time()
print(stop-start)
'''这个时候就不用担心给他传参数的问题了,wrapper收到什么参数都会原封不动交给index函数'''
结果:进行了一系列的优化,我们发现虽然传参的问题解决了,但是这个时候index函数也写死了,以后的需求中不可能只有它需要这个功能,复用性不够,因此可以继续优化。
5、方案五
优化:index写死了的问题
代码语言:javascript复制'''我们知道一旦某个变量写死了,那么我们就用一个变量去代替他,但是在wrapper函数中,index写成变量后,无法通过形参传给他,这个时候闭包函数就大显神威了,它就提供了一种给函数传参的方式'''
def outter(func):
#func = index #写活
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
stop = time.time()
print(stop-start)
return wrapper
f = outter(index)
'''返回的是wrapper的内存地址赋值给f,加个括号就是在调用wrapper,它的作用就是计算以函数对象传入其中的index的执行时间统计'''
# 为了不改变调用方式,在进行优化
# 可以把f = outter(index),为什么不可以赋值给index呢
# 最后:index = outter(index) 即wrapper的内存地址
index() # 此时对于函数的调用者来说,他没有变化,早就换了
6、方案五
优化:上面看是已经优化得差不多了,其实还是有漏洞,原函数index是没有返回值的,此时调用换掉之后的index之后,返回的时wrapper的内存地址,它并没有返回值,index()返回的是None,没有做到天衣无缝。这个时候就要用到我们上面讲到的函数对象的引用可以作为返回值,问题就迎刃而解了。
代码语言:javascript复制def outter(func):
#func = index #写活
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
stop = time.time()
print(stop-start)
return res
return wrapper
''' 我们把func函数的返回值通过return,在返回出来,当我们运行index()时,实际上就是在调用wrapper,此刻它是有返回值的,也就是func的返回值,这个时候才做到了天衣无缝'''
6、最终方案(推荐)
代码语言:javascript复制def outter(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
return res
return wrapper
这就是一个最简单的无参装饰器的模版,我们想要给某个对象也就是func函数添加新功能时直接在wrapper函数内部书写代码即可。
关于有参装饰器,此处由于篇幅限制就不在说明。有参也就说明我们的函数内部需要一个参数,无非就是两种方式,一种通过形参直接传入,另一种就是通过闭包函数直接包给它,此处肯定是利用闭包函数更合理。
六、感言
误打误撞,因为老师的一个课后作业任务,完成了本人的第一篇知识总结。刚开始是有点惊慌的,但是随之而来的是惊喜,虽然担心写的不够好,但也算是想给自己一个交代,一个好的开始。知识的持续分享总结能够促进我们持续的学习进步。
经过这次小小的分享,回到开头,我想说的就是学习一些高阶知识,当我们感到模模糊糊的时候,不妨回归本质,从最基础的原理对他进行分解,一步一步推导,往往能给到我们一种醍醐灌顶,意想不到的收获。最后希望同大家一道能通过这次的学习,提升自己,完成自己的初心。