如何使用函数装饰器?
实际案例
某些时候我们想为多个函数,统一添加某种功能,比如记时统计、记录日志、缓存运算结果等等。
我们不想在每个函数内一一添加完全相同的代码,有什么好的解决方案呢?
解决方案
定义装饰奇函数,用它来生成一个在原函数基础添加了新功能的函数,替代原函数
如有如下两道题:
题目一
斐波那契数列又称黄金分割数列,指的是这样一个数列:1,1,2,3,5,8,13,21,….,这个数列从第三项开始,每一项都等于前两项之和,求数列第n项。
题目二
一个共有10个台阶的楼梯,从下面走到上面,一次只能迈1-3个台阶,并且不能后退,走完整个楼梯共有多少种方法?
脚本如下:
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
33
34
35
|
# 函数装饰器 def memp(func):
cache = {}
def wrap( * args):
if args not in cache:
cache[args] = func( * args)
return cache[args]
return wrap
# 第一题 @memp def fibonacci(n):
if n < = 1 :
return 1
return fibonacci(n - 1 ) + fibonacci(n - 2 )
print (fibonacci( 50 ))
# 第二题 @memp def climb(n, steps):
count = 0
if n = = 0 :
count = 1
elif n > 0 :
for step in steps:
count + = climb(n - step, steps)
return count
print (climb( 10 , ( 1 , 2 , 3 )))
|
输出结果:
1
2
3
4
5
|
C:\Python\Python35\python.exe E: / python - intensive - training / s11.py
20365011074 274 Process finished with exit code 0
|
如何为被装饰的函数保存元数据?
实际案例
在函数对象张保存着一些函数的元数据,例如:
方法 | 描述 |
---|---|
f.__name__ |
函数的名字 |
f.__doc__ |
函数文档字符串 |
f.__module__ |
函数所属模块名 |
f.__dict__ |
属性字典 |
f.__defaults__ |
默认参数元素 |
我们在使用装饰器后,再使用上面的这些属性访问时,看到的是内部包裹函数的元数据,原来函数的元数据变丢失掉了,应该如何解决?
解决方案
使用标准库functools
中的装饰器wraps
装饰内部包裹函数,可以指定将原来函数的某些属性更新到包裹函数上面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
from functools import wraps
def mydecoratot(func):
@wraps(func)
def wrapper( * args, * * kwargs):
"""wrapper function"""
print ( "In wrapper" )
func( * args, * * kwargs)
return wrapper
@mydecoratot def example():
"""example function"""
print ( 'In example' )
print (example.__name__)
print (example.__doc__)
|
输出结果:
1
2
3
4
5
|
C:\Python\Python35\python.exe E: / python - intensive - training / s12.py
example example function Process finished with exit code 0
|
如何定义带参数的装饰器?
实际案例
实现一个装饰器,它用来检查被装饰函数的参数类型,装饰器可以通过指定函数参数的类型,调用时如果检测出类型不匹配则抛出异常,比如调用时可以写成如下:
1
2
3
|
@typeassert ( str , int , int )
def f(a, b, c):
......
|
或者
1
2
3
|
@typeassert (y = list )
def g(x, y):
......
|
解决方案
提取函数签名:inspect.signature()
带参数的装饰器,也就是根据参数定制化一个装饰器,可以看成生产装饰器的工厂,美的调用typeassert
,返回一个特定的装饰器,然后用他去装饰其他函数。
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
|
from inspect import signature
def typeassery( * ty_args, * * ty_kwargs):
def decorator(func):
# 获取到函数参数和类型之前的关系
sig = signature(func)
btypes = sig.bind_partial( * ty_args, * * ty_kwargs).arguments
def wrapper( * args, * * kwargs):
for name, obj in sig.bind( * args, * * kwargs).arguments.items():
if name in btypes:
if not isinstance (obj, btypes[name]):
raise TypeError( '"%s" must be "%s" ' % (name, btypes[name]))
return func( * args, * * kwargs)
return wrapper
return decorator
@typeassery ( int , str , list )
def f(a, b, c):
print (a, b, c)
# 正确的 f( 1 , 'abc' , [ 1 , 2 , 3 ])
# 错误的 f( 1 , 2 , [ 1 , 2 , 3 ])
|
执行结果
1
2
3
4
5
6
7
8
9
10
|
C:\Python\Python35\python.exe E: / python - intensive - training / s13.py
1 abc [ 1 , 2 , 3 ]
Traceback (most recent call last): File "E:/python-intensive-training/s13.py" , line 28 , in <module>
f( 1 , 2 , [ 1 , 2 , 3 ])
File "E:/python-intensive-training/s13.py" , line 14 , in wrapper
raise TypeError( '"%s" must be "%s" ' % (name, btypes[name]))
TypeError: "b" must be "<class 'str'>" Process finished with exit code 1
|
如何实现属性可修改的函数装饰器?
实际案例
为分析程序内那些函数执行时间开销较大,我们定义一个带timeout参数的函数装饰器,装饰功能如下:
-
统计被装饰函数单词调用运行时间
-
时间大于参数timeout的,将此次函数调用记录到log日志中
-
运行时可修改timeout的值
解决方案
为包裹函数增加一个函数,用来修改闭包中使用的*变量
在python3中使用nonlocal访问嵌套作用于中的变量引用
代码如下:
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
33
34
35
36
37
38
39
40
41
42
43
|
from functools import wraps
import time
import logging
from random import randint
def warn(timeout):
# timeout = [timeout] # py2
def decorator(func):
def wrapper( * args, * * kwargs):
start = time.time()
res = func( * args, * * kwargs)
used = time.time() - start
if used > timeout:
# if used > timeout: # py2
msg = '"%s": "%s" > "%s"' % (func.__name__, used, timeout)
# msg = '"%s": "%s" > "%s"' % (func.__name__, used, timeout[0]) # py2
logging.warn(msg)
return res
def setTimeout(k):
nonlocal timeout
timeout = k
# timeout[0] = k # py2
wrapper.setTimeout = setTimeout
return wrapper
return decorator
@warn ( 1.5 )
def test():
print ( 'In Tst' )
while randint( 0 , 1 ):
time.sleep( 0.5 )
for _ in range ( 10 ):
test()
test.setTimeout( 1 )
for _ in range ( 10 ):
test()
|
输出结果:
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
|
C:\Python\Python35\python.exe E: / python - intensive - training / s14.py
In Tst In Tst WARNING:root: "test" : "2.503000259399414" > "1.5"
In Tst In Tst In Tst In Tst In Tst In Tst In Tst In Tst In Tst WARNING:root: "test" : "1.0008063316345215" > "1"
In Tst In Tst In Tst WARNING:root: "test" : "1.0009682178497314" > "1"
In Tst In Tst WARNING:root: "test" : "1.5025172233581543" > "1"
In Tst In Tst In Tst In Tst Process finished with exit code 0
|
如何在类中定义装饰器?
实际案例
实现一个能将函数调用信息记录到日志的装饰器:
-
把每次函数的调用时间,执行时间,调用次数写入日志
-
可以对被装饰函数分组,调用信息记录到不同日志
-
动态修改参数,比如日志格式
-
动态打开关闭日志输出功能
解决方案
为了让装饰器在使用上更加灵活,可以把类的实例方法作为装饰器,此时包裹函数中就可以持有实例对象,便于修改属性和扩展功能
代码如下:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
import logging
from time import localtime, time, strftime, sleep
from random import choice
class CallingInfo:
def __init__( self , name):
log = logging.getLogger(name)
log.setLevel(logging.INFO)
fh = logging.FileHandler(name + '.log' ) # 日志保存的文件
log.addHandler(fh)
log.info( 'Start' .center( 50 , '-' ))
self .log = log
self .formattter = '%(func)s -> [%(time)s - %(used)s - %(ncalls)s]'
def info( self , func):
def wrapper( * args, * * kwargs):
wrapper.ncalls + = 1
lt = localtime()
start = time()
res = func( * args, * * kwargs)
used = time() - start
info = {}
info[ 'func' ] = func.__name__
info[ 'time' ] = strftime( '%x %x' , lt)
info[ 'used' ] = used
info[ 'ncalls' ] = wrapper.ncalls
msg = self .formattter % info
self .log.info(msg)
return res
wrapper.ncalls = 0
return wrapper
def SetFormatter( self , formatter):
self .formattter = formatter
def turnOm( self ):
self .log.setLevel(logging.INFO)
def turnOff( self ):
self .log.setLevel(logging.WARN)
cinfo1 = CallingInfo( 'mylog1' )
cinfo2 = CallingInfo( 'mylog2' )
# 设置日志指定格式 # cinfo1.SetFormatter('%(func)s -> [%(time)s - %(ncalls)s]') # 关闭日志 # cinfo2.turnOff() @cinfo1 .info
def f():
print ( 'in F' )
@cinfo1 .info
def g():
print ( 'in G' )
@cinfo2 .info
def h():
print ( 'in H' )
for _ in range ( 50 ):
choice([f, g, h])()
sleep(choice([ 0.5 , 1 , 1.5 ]))
|