装饰器(Decorators)是Python学习中迈不过的坎,也是编写出好的Python代码的一大利器,今天我们就由浅入深的学习这一技能包!
在学习装饰器之前,我们先对它的作用有一个大致的了解:装饰器用来以某种方式增强函数的行为。
不理解没关系,我们先来学习几个装饰器的例子。
基础学习:
一、装饰器初体验:
一个简单的装饰器:
eg1:
1 defdecorate(func):2 defnew_func():3 print("running new_func()")4 returnnew_func5
6 @decorate7 def prim_func(): #使用decorate装饰prim_func,
8 print("running prim_func()")9
10 prim_func()11
12 #output:running new_func() # 函数prim_func的功能变为new_func
解释:上例中decorate即为装饰器,其语法为:@+装饰器函数名。例子中prim_func()函数的定义其实相当于做了两件事:
1 defprim_func():2 print("running prim_func()") #定义prim_func函数
3
4 prim_func = decorate(prim_func) #装饰prim_func函数
上述装饰器将被装饰函数替换成另一个函数。实际上装饰器的两个主要的作用就是:
①、将被装饰函数替换成另一个函数或可调用对象(对应上例)。
②、处理被装饰的函数,然后将他返回。这种用法见下例:
eg2:
1 defdecorate(func):2 print("running decorate(%s)"%func)3 returnfunc4
5 @decorate6 def prim_func(time): #装饰
7 print("running prim_func()","第%d次"%time)8
9 prim_func(1) #第一次调用
10 prim_func(2) #第二次调用
11
12 #output:
13 #running decorate()
14 #running prim_func() 第1次
15 #running prim_func() 第2次
两个例子一对比,不难产生如下疑问:
同样是装饰器,为什么eg1采用了函数嵌套,eg2不使用函数嵌套?
答:eg1中创建了新的函数new_func,增强了原函数prim_func函数的功能,用以对prim_func做修改;而eg2中没有对原函数prim_func做修改,故不需要创建新的函数也就不需要嵌套另外一个函数。
为什么eg2中函数第一次调用有输出“running decorate()”,第二次调用没有?
答:这其实涉及到何时执行装饰器的问题,例子中的输出“running decorate()”并不是由于函数prim_func的调用,而是由于装饰器的执行。函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。所以例子中装饰器的运行是在所有函数调用之前,和prim_runc的调用无关。
二、叠放装饰器:
有时候我们会看到一个函数被两个甚至多个装饰器装饰,其实也很好理解,例如:
1 @d12 @d23 deff():4 print('running f()')
等同于:
1 deff():2 print('running f()')3
4 f = d1(d2(f))
所以,不要被它的外表给吓到了,多个装饰器的叠放可以此类推……
典型应用:
一、注册装饰器改进“策略”模式
问题背景:《设计模式:可复用面向对象软件的基础》一书是这样描述“策略”模式的:定义一系列算法,把他们一一封装起来,并且使他们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。(不理解没关系,我们看下面一个例子:)
具体问题:假设电商领域某个网店制定了三种折扣规则,顾客每次只能享受一个折扣,如何设计程序选择最佳折扣?
普通函数式解法:
1 promos =[promotion_1, promotion_2, promotion_3]2
3 def promotion_1(order): #第一种折扣
4 pass
5
6 def promotion_2(order): #第二种折扣
7 pass
8
9 def promotion_3(order): #第三种折扣
10 pass
11
12 def best_promo(order): #选择可用的最佳折扣
13 return max(promot(order) for promo in promos)
上例中我们为三种不同折扣分别创建一个函数(promotion_x)计算优惠量,并创建一个函数列表(promos)保存不同的折扣函数,最后创建一个函数(best_promo)用来选择函数列表中的最佳折扣。
上例易于阅读,而且可以达到设计目的,但也有些不易察觉的缺陷:若想添加新的促销策略,要定义相应的函数,还要记得把它添加到promos列表中;否则,当添加新促销后,best_promo不会考虑到它。通过注册装饰器可以很轻松地解决这个问题,请看例子:
1 promos = [] #列表起初是空的
2
3 def promotion(promo_func): #装饰器在模块导入时就将所有折扣策略添加到列表promos中
4 promos.append(promo_func)5 return promo_func #将原函数原封不动地返回
6
7 @promotion8 def promotion_1(order): #第一种折扣
9 pass
10
11 @promotion12 def promotion_2(order): #第二种折扣
13 pass
14
15 @promotion16 def promotion_3(order): #第三种折扣
17 pass
18
19 def best_promo(order): #选择可用的最佳折扣
20 return max(promot(order) for promo in promos)
这种方法不仅完美地解决了上述问题,还有如下优点:
@promotion装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略:只需要把装饰器注释掉。
促销折扣策略可以在其他模块中定义,在系统的任何地方都行,只要使用@promotion装饰即可。
二、另一个例子:
下例实现一个简单的装饰器,在每次调用函数的装饰器时计时,然后把经过的时间、传入的参数和调用的结果打印出来。
1 importtime2
3 defclock(func):4 def clocked(*args):5 t0 =time.perf_counter()6 result = func(*args) #clocked的闭包中包含自由变量func
7 tim = time.perf_counter() -t08 name = func.__name__
9 arg_str = ','.join(repr(arg) for arg inargs)10 print('[%0.8fs] %s(%s) -> %r' %(tim, name ,arg_str, result))11 returnresult12 return clocked #返回内部函数,取代被装饰的函数
13
14 @clock15 defadd(data1, data2, data3):16 return data1 + data2 +data317
18 if __name__ == '__main__':19 a = add(1, 3, 5)20 print(a)
21
22 # output:
23 # [0.00000100s] add(1, 3, 5) -> 9
24 # 9
本例中我们实现了一个简单的装饰器,增强了原函数的行为。从上例中,可以学到:①、原函数为带参函数时,我们可以通过可变参数实现装饰器。②、原函数有返回值时,在创建新函数中执行原函数,并将返回值返回。
下面是一个装饰器的模板,用来装饰带有参数,并且有返回值的函数。
1 defdecorator(func):2 def inner(*args, **kwargs): #可变参数
3 print('add inner called')4 result = func(*args, **kwargs) #执行原函数
5 return result6 returninner7
8 @decorator9 defadd(a, b):10 return a +b11
12 @decorator13 defadd2(a, b, c):14 return a + b +c15
16 print(add(2, 4))17 print(add2(2, 4, 6))
现在,我们基本了解了装饰器,也能够自己实现一些装饰器来改善自己的代码。那我们就完全掌握装饰器了吗?还没有,还需要我们不断深入。
装饰器进阶:
一、参数化装饰器(带参数的装饰器):
通过之前的函数我们了解到,装饰器的参数通常为被装饰的函数,那装饰器还能接受其他参数吗?怎么让装饰器接收其他参数呢?答案是:使用带参数的装饰器:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。不明白?请看例子:
1 importtime2
3 DEFAULT_FMT = '[%0.8fs] %s(%s) -> %r'
4
5 def clock(fmt=DEFAULT_FMT): #装饰器工厂函数
6 def decorate(func): #装饰器
7 def clocked(*args):8 t0 =time.perf_counter()9 result = func(*args)
10 tim = time.perf_counter() -t011 name = func.__name__
12 arg_str = ','.join(repr(arg) for arg inargs)13 print(fmt %(tim, name ,arg_str, result))14 returnresult15 return clocked #返回内部函数,取代被装饰的函数
16 returndecorate17
18 @clock() #clock是装饰器工厂函数,必须作为函数调用
19 defadd(data1, data2, data3):20 return data1 + data2 +data321
22 @clock('test:[%0.8fs] %s(%s) -> %r') #其实相当于add2 = clock('test:[%0.8fs] %s(%s) -> %r ')(add2)
23 defadd2(data1, data2, data3):24 return data1 + data2 +data325
26 if __name__ == '__main__':27 a = add(1, 3, 5)28 print(a)29
30 b = add2(1, 3, 5)31 print(b)32
33 #output:
34 #[0.00000090s] add(1, 3, 5) -> 9
35 #9
36 #test:[0.00000040s] add2(1, 3, 5) -> 9
37 #9
本例只是对前面的例子做了一个简单的改造,可以看出,带参数的装饰器其实就是一个装饰器工厂函数,通过为装饰器工厂函数赋不同的值,可以得到不同的装饰器。
二、类装饰器:
< 待添加……>