装饰器(Decorators)是Python学习中迈不过的坎,也是编写出好的Python代码的一大利器,今天我们就由浅入深的学习这一技能包!
在学习装饰器之前,我们先对它的作用有一个大致的了解:装饰器用来以某种方式增强函数的行为。
不理解没关系,我们先来学习几个装饰器的例子。
基础学习:
一、装饰器初体验:
一个简单的装饰器:
eg1:
1 def decorate(func): 2 def new_func(): 3 print("running new_func()") 4 return new_func 5 6 @decorate 7 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 def prim_func(): 2 print("running prim_func()") # 定义prim_func函数 3 4 prim_func = decorate(prim_func) # 装饰prim_func函数
上述装饰器将被装饰函数替换成另一个函数。实际上装饰器的两个主要的作用就是:
①、将被装饰函数替换成另一个函数或可调用对象(对应上例)。
②、处理被装饰的函数,然后将他返回。这种用法见下例:
eg2:
1 def decorate(func): 2 print("running decorate(%s)"%func) 3 return func 4 5 @decorate 6 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(<function prim_func at 0x008AA270>) 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(<function prim_func at 0x008AA270>)”,第二次调用没有?
答:这其实涉及到何时执行装饰器的问题,例子中的输出“running decorate(<function prim_func at 0x008AA270>)”并不是由于函数prim_func的调用,而是由于装饰器的执行。函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用时运行。所以例子中装饰器的运行是在所有函数调用之前,和prim_runc的调用无关。
二、叠放装饰器:
有时候我们会看到一个函数被两个甚至多个装饰器装饰,其实也很好理解,例如:
1 @d1 2 @d2 3 def f(): 4 print('running f()')
等同于:
1 def f(): 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 @promotion 8 def promotion_1(order): # 第一种折扣 9 pass 10 11 @promotion 12 def promotion_2(order): # 第二种折扣 13 pass 14 15 @promotion 16 def promotion_3(order): # 第三种折扣 17 pass 18 19 def best_promo(order): # 选择可用的最佳折扣 20 return max(promot(order) for promo in promos)
这种方法不仅完美地解决了上述问题,还有如下优点:
- @promotion装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略:只需要把装饰器注释掉。
- 促销折扣策略可以在其他模块中定义,在系统的任何地方都行,只要使用@promotion装饰即可。
二、另一个例子:
下例实现一个简单的装饰器,在每次调用函数的装饰器时计时,然后把经过的时间、传入的参数和调用的结果打印出来。
1 import time 2 3 def clock(func): 4 def clocked(*args): 5 t0 = time.perf_counter() 6 result = func(*args) # clocked的闭包中包含自由变量func 7 tim = time.perf_counter() - t0 8 name = func.__name__ 9 arg_str = ', '.join(repr(arg) for arg in args) 10 print('[%0.8fs] %s(%s) -> %r ' % (tim, name ,arg_str, result)) 11 return result 12 return clocked # 返回内部函数,取代被装饰的函数 13 14 @clock 15 def add(data1, data2, data3): 16 return data1 + data2 + data3 17 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 def decorator(func): 2 def inner(*args, **kwargs): # 可变参数 3 print('add inner called') 4 result = func(*args, **kwargs) # 执行原函数 5 return result 6 return inner 7 8 @decorator 9 def add(a, b): 10 return a + b 11 12 @decorator 13 def add2(a, b, c): 14 return a + b + c 15 16 print(add(2, 4)) 17 print(add2(2, 4, 6))
现在,我们基本了解了装饰器,也能够自己实现一些装饰器来改善自己的代码。那我们就完全掌握装饰器了吗?还没有,还需要我们不断深入。
装饰器进阶:
一、参数化装饰器(带参数的装饰器):
通过之前的函数我们了解到,装饰器的参数通常为被装饰的函数,那装饰器还能接受其他参数吗?怎么让装饰器接收其他参数呢?答案是:使用带参数的装饰器:创建一个装饰器工厂函数,把参数传给它,返回一个装饰器,然后再把它应用到要装饰的函数上。不明白?请看例子:
1 import time 2 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() - t0 11 name = func.__name__ 12 arg_str = ', '.join(repr(arg) for arg in args) 13 print(fmt % (tim, name ,arg_str, result)) 14 return result 15 return clocked # 返回内部函数,取代被装饰的函数 16 return decorate 17 18 @clock() # clock是装饰器工厂函数,必须作为函数调用 19 def add(data1, data2, data3): 20 return data1 + data2 + data3 21 22 @clock('test:[%0.8fs] %s(%s) -> %r ') # 其实相当于add2 = clock('test:[%0.8fs] %s(%s) -> %r ')(add2) 23 def add2(data1, data2, data3): 24 return data1 + data2 + data3 25 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
本例只是对前面的例子做了一个简单的改造,可以看出,带参数的装饰器其实就是一个装饰器工厂函数,通过为装饰器工厂函数赋不同的值,可以得到不同的装饰器。
二、类装饰器:
< 待添加……>