本节主要介绍编写函数装饰器的相关内容。
跟踪调用
如下代码定义并应用一个函数装饰器,来统计对装饰的函数的调用次数,并且针对每一次调用打印跟踪信息。
1
2
3
4
5
6
7
8
9
10
11
|
class tracer: def __init__( self ,func): self .calls = 0 self .func = func def __call__( self , * args): self .calls + = 1 print ( 'call %s to %s' % ( self .calls, self .func.__name__)) self .func( * args) @tracer def spam(a, b, c): print (a + b + c) |
这是一个通过类装饰的语法写成的装饰器,测试如下:
1
2
3
4
5
6
7
8
9
10
|
>>> spam( 1 , 2 , 3 ) call 1 to spam 6 >>> spam( 'a' , 'b' , 'c' ) call 2 to spam abc >>> spam.calls 2 >>> spam <__main__.tracer object at 0x03098410 > |
运行的时候,tracer类和装饰的函数分开保存,并且拦截对装饰的函数的随后的调用,以便添加一个逻辑层来统计和打印每次调用。
装饰之后,spam实际上是tracer类的一个实例。
@装饰器语法避免了直接地意外调用最初的函数。考虑如下所示的非装饰器的对等代码:
1
2
3
4
5
6
7
8
|
calls = 0 def tracer(func, * args): global calls calls + = 1 print ( 'call %s to %s' % (calls,func.__name__)) func( * args) def spam(a,b,c): print (a + b + c) |
测试如下:
1
2
3
4
5
6
7
8
9
10
11
|
? 1 2 3 4 5 >>> spam( 1 , 2 , 3 ) 6 >>> tracer(spam, 1 , 2 , 3 ) call 1 to spam 6 |
这一替代方法可以用在任何函数上,且不需要特殊的@语法,但是和装饰器版本不同,它在代码中调用函数的每个地方都需要额外的语法。尽管装饰器不是必需的,但是它们通常是最为方便的。
扩展——支持关键字参数
下述代码时前面例子的扩展版本,添加了对关键字参数的支持:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class tracer: def __init__( self ,func): self .calls = 0 self .func = func def __call__( self , * args, * * kargs): self .calls + = 1 print ( 'call %s to %s' % ( self .calls, self .func.__name__)) self .func( * args, * * kargs) @tracer def spam(a, b, c): print (a + b + c) @tracer def egg(x,y): print (x * * y) |
测试如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
>>> spam( 1 , 2 , 3 ) call 1 to spam 6 >>> spam(a = 4 ,b = 5 ,c = 6 ) call 2 to spam 15 >>> egg( 2 , 16 ) call 1 to egg 65536 >>> egg( 4 ,y = 4 ) call 2 to egg 256 |
也可以看到,这里的代码同样使用【类实例属性】来保存状态,即调用的次数self.calls。包装的函数和调用计数器都是针对每个实例的信息。
使用def函数语法写装饰器
使用def定义装饰器函数也可以实现相同的效果。但是有一个问题,我们也需要封闭作用域中的一个计数器,它随着每次调用而更改。我们可以很自然地想到全局变量,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
calls = 0 def tracer(func): def wrapper( * args, * * kargs): global calls calls + = 1 print ( 'call %s to %s' % (calls,func.__name__)) return func( * args, * * kargs) return wrapper @tracer def spam(a,b,c): print (a + b + c) @tracer def egg(x,y): print (x * * y) |
这里calls定义为全局变量,它是跨程序的,是属于整个模块的,而不是针对每个函数的,这样的话,对于任何跟踪的函数调用,计数器都会递增,如下测试:
1
2
3
4
5
6
7
8
9
10
11
12
|
>>> spam( 1 , 2 , 3 ) call 1 to spam 6 >>> spam(a = 4 ,b = 5 ,c = 6 ) call 2 to spam 15 >>> egg( 2 , 16 ) call 3 to egg 65536 >>> egg( 4 ,y = 4 ) call 4 to egg 256 |
可以看到针对spam函数和egg函数,程序用的是同一个计数器。
那么如何实现针对每一个函数的计数器呢,我们可以使用Python3中新增的nonlocal语句,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
def tracer(func): calls = 0 def wrapper( * args, * * kargs): nonlocal calls calls + = 1 print ( 'call %s to %s' % (calls,func.__name__)) return func( * args, * * kargs) return wrapper @tracer def spam(a,b,c): print (a + b + c) @tracer def egg(x,y): print (x * * y) spam( 1 , 2 , 3 ) spam(a = 4 ,b = 5 ,c = 6 ) egg( 2 , 16 ) egg( 4 ,y = 4 ) |
运行如下:
1
2
3
4
5
6
7
8
|
call 1 to spam 6 call 2 to spam 15 call 1 to egg 65536 call 2 to egg 256 |
这样,将calls变量定义在tracer函数内部,使之存在于一个封闭的函数作用域中,之后通过nonlocal语句来修改这个作用域,修改这个calls变量。如此便可以实现我们所需求的功能。
陷阱:装饰类方法
【注意,使用类编写的装饰器不能用于装饰某一类中带self参数的的函数,这一点在Python装饰器基础中介绍过】
即如果装饰器是如下使用类编写的:
1
2
3
4
5
6
7
8
|
class tracer: def __init__( self ,func): self .calls = 0 self .func = func def __call__( self , * args, * * kargs): self .calls + = 1 print ( 'call %s to %s' % ( self .calls, self .func.__name__)) return self .func( * args, * * kargs) |
当它装饰如下在类中的方法时:
1
2
3
4
5
6
7
|
class Person: def __init__( self ,name,pay): self .name = name self .pay = pay @tracer def giveRaise( self ,percent): self .pay * = ( 1.0 + percent) |
这时程序肯定会出错。问题的根源在于,tracer类的__call__方法的self——它是一个tracer实例,当我们用__call__把装饰方法名重绑定到一个类实例对象的时候,Python只向self传递了tracer实例,它根本没有在参数列表中传递Person主体。此外,由于tracer不知道我们要用方法调用处理的Person实例的任何信息,没有办法创建一个带有一个实例的绑定的方法,所以也就没有办法正确地分配调用。
这时我们只能通过嵌套函数的方法来编写装饰器。
计时调用
下面这个装饰器将对一个装饰的函数的调用进行计时——既有针对一次调用的时间,也有所有调用的总的时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
import time class timer: def __init__( self ,func): self .func = func self .alltime = 0 def __call__( self , * args, * * kargs): start = time.clock() result = self .func( * args, * * kargs) elapsed = time.clock() - start self .alltime + = elapsed print ( '%s:%.5f,%.5f' % ( self .func.__name__,elapsed, self .alltime)) return result @timer def listcomp(N): return [x * 2 for x in range (N)] @timer def mapcall(N): return list ( map (( lambda x :x * 2 ), range (N))) result = listcomp( 5 ) listcomp( 50000 ) listcomp( 500000 ) listcomp( 1000000 ) print (result) print ( 'allTime = %s' % listcomp.alltime) print ('') result = mapcall( 5 ) mapcall( 50000 ) mapcall( 500000 ) mapcall( 1000000 ) print (result) print ( 'allTime = %s' % mapcall.alltime) print ( 'map/comp = %s ' % round (mapcall.alltime / listcomp.alltime, 3 )) |
运行结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
listcomp: 0.00001 , 0.00001 listcomp: 0.00885 , 0.00886 listcomp: 0.05935 , 0.06821 listcomp: 0.11445 , 0.18266 [ 0 , 2 , 4 , 6 , 8 ] allTime = 0.18266365607537918 mapcall: 0.00002 , 0.00002 mapcall: 0.00689 , 0.00690 mapcall: 0.08348 , 0.09038 mapcall: 0.16906 , 0.25944 [ 0 , 2 , 4 , 6 , 8 ] allTime = 0.2594409060462425 map / comp = 1.42 |
这里要注意的是,map操作在Python3中返回一个迭代器,所以它的map操作不能和一个列表解析的工作直接对应,即实际上它并不花时间。所以要使用list(map())来迫使它像列表解析那样构建一个列表
添加装饰器参数
有时我们需要装饰器来做一个额外的工作,比如提供一个输出标签并且可以打开或关闭跟踪消息。这就需要用到装饰器参数了,我们可以使用装饰器参数来制定配置选项,这些选项可以根据每个装饰的函数而编码。例如,像下面这样添加标签:
1
2
3
4
5
6
7
8
9
|
def timer(label = ''): def decorator(func): def onCall( * args): ... print (label,...) return onCall return decorator @timer ( '==>' ) def listcomp(N):... |
我们可以将这样的结果用于计时器中,来允许在装饰的时候传入一个标签和一个跟踪控制标志。比如,下面这段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import time def timer(label = '', trace = True ): class Timer: def __init__( self ,func): self .func = func self .alltime = 0 def __call__( self , * args, * * kargs): start = time.clock() result = self .func( * args, * * kargs) elapsed = time.clock() - start self .alltime + = elapsed if trace: ft = '%s %s:%.5f,%.5f' values = (label, self .func.__name__,elapsed, self .alltime) print ( format % value) return result return Timer |
这个计时函数装饰器可以用于任何函数,在模块中和交互模式下都可以。我们可以在交互模式下测试,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
>>> @timer(trace = False ) def listcomp(N): return [x * 2 for x in range (N)] >>> x = listcomp( 5000 ) >>> x = listcomp( 5000 ) >>> x = listcomp( 5000 ) >>> listcomp <__main__.timer.< locals >.Timer object at 0x036DCC10 > >>> listcomp.alltime 0.0011475424533080223 >>> >>> @timer(trace = True ,label = '\t=>' ) def listcomp(N): return [x * 2 for x in range (N)] >>> x = listcomp( 5000 ) = > listcomp: 0.00036 , 0.00036 >>> x = listcomp( 5000 ) = > listcomp: 0.00034 , 0.00070 >>> x = listcomp( 5000 ) = > listcomp: 0.00034 , 0.00104 >>> listcomp.alltime 0.0010432902706075842 < / locals > |
有关Python编写函数装饰器相关知识小编就给大家介绍到这里,希望对大家有所帮助!