装饰器
https://www.zhihu.com/question/26930016
一、问题提出
有如下二个函数,(为什么这里函数要取名为wrapped后面讲):
def wrapped1():
print('Run wrapped1!')
def wrapped2():
print('Run wrapped2')
现在有一个新要求:执行这两个函数时都打印信息“Program start!”。
如何解决?
直接能想到的办法是分别在两个函数中加入print('Program start!'),像下面这样:
def wrapped1():
print('Program start!')
print('Run wrapped1!')
def wrapped2():
print('Program start!')
print('Run wrapped2')
但是这存在两个问题:
1)如果有10000个这样的函数,岂不是每个都要分别加代码,工作量极其巨大。
2)在函数中新增代码,势必会破坏原函数的完整性。
针对上述问题,有人提出,可以重新写一个函数new_func(),然后将这些不能动的函数作为new_func()的参数,在new_func()中先执行print('Program start!'),再执行对应的函数,像下面这样:
def new_func(func):
print("Program start!")
func()
new_func(wrapped1)
new_func(wrapped2)
这么做好像可行,但出现了另一个问题:
3)破坏了程序结构,原来用wrapped1()的地方,现在得改成new_func(wrapped1)。
再考虑另一种实现方式,前面我们学过闭包,也可以实现上述过程,如下例。下述代码中wrapper是一个闭包函数。下述代码虽然也实现了要求,但同样存在问题3),无法直接通过wrapped1()执行函数。(注意 wrapper 的 *args 与**kwargs 参数, 保证了能完全传递所有参数,之后再将其传到 func函数中。)
def decorator(func):
def wrapper(*args, **kwargs):
print ('Program start!')
return func(*args, **kwargs)
return wrapper
def wrapped1():
print('Run wrapped1!')
def wrapped2():
print('Run wrapped2')
#####################################################
my_wrapped1 = decorator(wrapped1)
my_wrapped1()
二、函数装饰器
前面这段代码本质上已经是所谓的“装饰器”的效果了,只是在表现形式上还不够简洁,python为了让代码更简洁,创造了神奇的“@”语法糖,直接上代码:
def decorator(func):
def wrapper(*args, **kwargs):
print ('Program start!')
return func(*args, **kwargs)
return wrapper
@decorator
def wrapped1():
print('Run wrapped1!')
@decorator
def wrapped2():
print('Run wrapped2')
#####################################################
wrapped1()
wrapped2()
#输出
Program start!
Run wrapped1!
Program start!
Run wrapped2
上述代码展示了一个简单但完整的装饰器功能。比较本文最前的代码,此时我们执行wrapped1()和wrapped2(),都会先输出“Program start!”,但我们并没有对wrapped1和wrapped2函数做任何修改,只是在其定义前加了“@decorator”。
上述代码中decorator()和wrapper()形成了闭包,其中wrapper()是闭包函数。decorator()的输入参数func是函数名,这个函数是要在闭包函数wrapper()中执行的。@decorator的作用就是去装饰其他函数(说白了就是把这句代码写在任意其他函数的定义之前),他将被装饰的函数传入decorator()。
整个过程:
1)decorator()函数是装饰器,他把要装饰和被装饰的两方联在一起;
2)wrapper()正是那个要装饰,即把wrapper中的某些东西装饰给别人(例如这里的print ('Program start!'));
3)wrapped1和wrapped2就是被装饰,你看他们顶上多了个帽子“@decorator”;
4)结果是执行wrapped1()和wrapped2()时,平白无故多出了'Program start!'。
前述代码中,函数wrapped1和wrapped2都是没有参数的函数,如果有参数怎么办?
答案是:完全不影响!但不要忘记参数*args和**kwargs,否则出错。
def decorator(func):
def wrapper(*args, **kwargs):
print ('Program start!')
return func(*args, **kwargs)
return wrapper
@decorator
def wrapped1(a,b):
print(a+b)
###########################################
wrapped1(5,3)
#输出
Program start!
8
三、装饰器的作用
(1)避免重复写公共性代码,提高代码复用性,如这里的“Program start!”;
(2)在不改动原函数的前提下,为原函数增加了额外功能,如这里原来执行wrapped1(),只输出Run wrapped1!,现在还输出Program start!;
(3)在给原函数增加额外功能的同时,不破坏源程序的结构,如这里原来执行wrapped1()的地方,现在还是wrapped1()。
简单点就是,装饰器给已有的模块(函数、类)加上一些其他功能(这些功能可能好多模块都会用到),但又不需要在已有模块内部增加额外代码。
装饰器最大的优势在于解决重复性操作,即将重复性代码写在装饰器中,谁想用就通过装饰器直接用即可,你只需要在每个函数上方加一个"@装饰器"即可。比如下面这个计算每个函数运行时间的装饰器:
import time
def run_time(func):
def wrapper(*args,**kwargs):
start = time.time()
func(*args,**kwargs) # 函数在这里运行
end = time.time()
cost_time = end - start
print("func three run time {}".format(cost_time))
return wrapper
@run_time
def fun_one():
time.sleep(1)
@run_time
def fun_two():
time.sleep(2)
@run_time
def fun_three():
time.sleep(3)
####################
fun_one()
fun_two()
fun_three()
#输出
func three run time 1.0002892017364502
func three run time 2.000812292098999
func three run time 3.0008528232574463
装饰器的本质是闭包
四、装饰器的实际执行过程
def decorator(func):
def wrapper(*args, **kwargs):
print ('Program start!')
return func(*args, **kwargs)
return wrapper
@decorator
def wrapped1(a,b):
print(a+b)
###########################################
wrapped1(5,3)
#输出
Program start!
8
上例中,最后我们执行了wrapped1(5,3),这里的wrapped虽然和定义时def wrapped1(a,b)的函数名相同,但实际上他们只是同名,已经不是定义时的那个wrapped1了。
因为本质上,装饰器是将原函数wrapped1作为装饰器函数decorator的一个参数,然后返回闭包函数wrapper,这个闭包函数会用到传进来的wrapped1函数,将wrapped1函数放在闭包函数内执行。
闭包的执行过程相当于:wrapped1=decorator(wrapped1),decorator(wrapped1)的返回值是闭包函数wrapper,相当于wrapped1=wrapper,可见,函数被装饰后,虽然与原函数同名,但实际上是装饰器内部的闭包函数。
五、带参数的装饰器
前面的装饰器,装饰的内容是“Program start!”,是固定不变的, 如果我们想动态指定装饰内容,那该怎么办?总不能老是去改装饰器函数吧。
我们可以这么做,如下例,在装饰器外部再套上一层函数info,用该函数的参数接收我们想要装饰的内容value,并将先前的 decorator 函数作为info的返回值。实际上是再生成了一个闭包结构,decorator与info组成闭包结构,wrapper与decorator组成闭包结构。
如此一来,只需要修改@info('Program 1 start!')中的内容就可以了。
def info(value):
def decorator(func):
def wrapper(*args, **kwargs):
print (value)
return func(*args, **kwargs)
return wrapper
return decorator
@info('Program 1 start!') #只要修改这里的值即可
def wrapped1(a,b):
print(a+b)
@info('Program 2 start!') #只要修改这里的值即可
def wrapped2(a,b):
print(a-b)
###########################################
wrapped1(5,3)
wrapped2(5,3)
#输出
Program 1 start!
8
Program 2 start!
2
六、wraps装饰器
一个函数不止有执行语句,还有__name__(函数名)等其他属性,例如:
def wrapped1(a,b):
print(a+b)
print(wrapped1.__name__)
#输出
wrapped1
但前面的装饰器会将原函数的这些信息改变,比如下例中,本来输出的是“wrapped1”,经过装饰器后变成了“wrapper”,即变成了装饰函数的名称了:
def decorator(func):
def wrapper(*args, **kwargs):
print (value)
return func(*args, **kwargs)
return wrapper
@decorator
def wrapped1(a,b):
print(a+b)
print(wrapped1.__name__)
#输出
wrapper
解决这一问题的办法是通过 functools 模块下的 wraps 装饰器:
from functools import wraps
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print (value)
return func(*args, **kwargs)
return wrapper
@decorator
def wrapped1(a,b):
print(a+b)
print(wrapped1.__name__)
#输出
wrapped1
七、多层装饰器及执行顺序
先看下例:
def decorator(func):
print('Run decorator!')
def wrapper(*args, **kwargs):
print('有火眼金睛了')
return func(*args, **kwargs)
return wrapper
print('cgx_test 1')
@decorator
def monkey_sun():
print('吃桃子')
#####################################
print('cgx_test 2')
monkey_sun()
#输出
cgx_test 1
Run decorator!
cgx_test 2
有火眼金睛了
吃桃子
该例的装饰器与前面并没不同,只是在装饰器函数decorator内部增加了一句 print('Run decorator!');
程序中print('cgx_test 1')和print('cgx_test 2')是用于测试的标志。
从输出结果可以看出,装饰器函数decorator在执行monkey_sun()之前就被执行了(因为输出了Run decorator!),或者说当程序执行到装饰器语法糖@时,装饰器函数decorator就被执行了:
@decorator
def monkey_sun():
即当程序执行完上两行代码时,实际上完成了decorator(func),我们知道装饰函数decorator的返回值是其内部闭包函数wrapper,也就意味着上述代代码执行完后,生成了一个wrapper函数,只是wrapper函数还没有被执行,wrapper的执行需要等到monkey_sun()这句话被执行,前面我们也分析过,最后的monkey_sun()本质就是wrapper函数,而非原来定义的monkey_sun函数。
了解了上述执行过程后,对于多重装饰就好理解了,见下例:
def decorator1(func):
print('Run decorator1!')
def wrapper1(*args, **kwargs):
print('有火眼金睛了')
return func(*args, **kwargs)
return wrapper1
def decorator2(func):
print('Run decorator2!')
def wrapper2(*args, **kwargs):
print('有金箍棒了')
return func(*args, **kwargs)
return wrapper2
def decorator3(func):
print('Run decorator3!')
def wrapper3(*args, **kwargs):
print('有72变了')
return func(*args, **kwargs)
return wrapper3
print('cgx_test 1')
@decorator1
@decorator2
@decorator3
def monkey_sun():
print('吃桃子')
#####################################
print('cgx_test 2')
monkey_sun()
#输出
cgx_test 1
Run decorator3!
Run decorator2!
Run decorator1!
cgx_test 2
有火眼金睛了
有金箍棒了
有72变了
吃桃子
该例中有三个装饰器decorator1-3,且在每个decorator函数内增加了一句“print('Run decorator*!')”,三个装饰器按顺序装饰了monkey_sun函数。
根据前面分析,装饰器函数decorator1-3会在语法糖@位置就被执行,从输出结果看确实是这样,因为在cgx_test 1和cgx_test 2之间输出了Run decorator3!、Run decorator2!、Run decorator1!。
根据输出结果的顺序可以判断,越是靠近被装饰函数的装饰器函数,越早执行,注意这句话的意思是装饰器函数decorator被执行,而非其内部的wrapper闭包函数,闭包函数只是decorator被执行后的返回结果,但还没执行它,这点特别要注意,从输出结果也可以看出,wrapper内的信息并没有在cgx_test 1和cgx_test 2之间输出。
接着我们执行了monkey_sun(),输出结果首先是“有火眼金睛”,可见是decorator1中的wrapper1最先执行,为什么反而是离被装饰函数最远的装饰器函数中的闭包最先执行呢?其实很好理解。
前面我们说了,离被装饰函数最近的装饰器函数最先被执行,即:
首先执行decorator3,它以monkey_sun函数作为输入(即decorator3(monkey_sun)),并返回了wrapper3;
然后执行decorator2,注意,此时它以上一步返回的wrapper3作为输入(即decorator2(wrapper3),并返回了wrapper2;
再执行decorator1,注意,此时它以上一步返回的wrapper2作为输入(即decorator1(wrapper2),并返回了wrapper1;
也就是说,最终我们得到的是wrapper1闭包函数;
根据前面分析,最后执行的monkey_sun(),本质上就是执行wrapper1(),因此首先输出了“有火眼金睛了”,而wrapper1中return func(*args, **kwargs)中的func是wrapper2,即着这句话就是执行wrapper2(),自然就输出了“有金箍棒了”,而wrapper2中return func(*args, **kwargs)中的func是wrapper3,即着这句话就是执行wrapper3(),自然就输出了“有72变”,而wrapper3中return func(*args, **kwargs)中的func才是monkey_sun,即着这句话就是执行monkey_sun(),自然就输出了“吃桃子”.
至此,整个装饰器的工作才算结束!
上述过程如下图所示:
八、类中的内置装饰器
有三种我们经常会用到的装饰器, @classmethod、 @staticmethod、 @property,他们有个共同点,都是作用于类方法之上。
详见:https://www.jianshu.com/writer#/notebooks/29416844/notes/92665962/preview
九、类装饰器
类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器还可以依靠类内部的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。
详见:https://www.jianshu.com/writer#/notebooks/29416844/notes/92665962/preview
网友评论