如何使用函数装饰器?
实际案例
某些时候我们想为多个函数,统一添加某种功能,比如记时统计、记录日志、缓存运算结果等等。
我们不想在每个函数内一一添加完全相同的代码,有什么好的解决方案呢?
解决方案
定义装饰奇函数,用它来生成一个在原函数基础添加了新功能的函数,替代原函数
如有如下两道题:
题目一
斐波那契数列又称黄金分割数列,指的是这样一个数列: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
]))
|