Python的装饰器

在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

参考链接 廖雪峰 Pipe somenzz

背景

为何会有装饰器?

在Python里面,一切皆对象,函数也是一种对象,可以被赋值给变量,所以,通过变量也能调用该函数。

1
2
3
4
5
6
7
>>> import time
>>> def now():
... print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
...
>>> f = now
>>> f()
2020-04-17 12:48:29

假设我们要增强now()函数的功能,比如,在函数调用前后自动打印日志或检查用户身份,但又不希望修改now()函数的定义,这种在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。

定义装饰器

本质上,decorator就是一个返回函数的高阶函数。所以,我们要定义一个能打印日志的decorator,可以定义如下:

1
2
3
4
5
>>> def log(func):
... def wrapper(*args, **kw):
... print('call: %s()' % func.__name__)
... return func(*args, **kw)
... return wrapper

使用

log 是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:

1
2
3
4
5
6
>>> @log  # 等价于 now = log(now)           
... def now():
... print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
>>> now()
call: now()
2020-04-17 12:58:09

wrapper()函数的参数定义是(*args, **kw),因此,wrapper()函数可以接受任意参数的调用。在wrapper()函数内,首先打印日志,再紧接着调用原始函数。

理解 @

@log放到now()函数的定义处,相当于执行了语句:

1
now = log(now)

由于log()是一个decorator,返回一个函数,所以,原来的now()函数仍然存在,只是现在同名的now变量指向了新的函数,也就是log(now),于是调用now()将执行新函数log(now),这个新函数就是log()函数中返回的wrapper()

装饰器传参

如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:

1
2
3
4
5
6
7
def log(text):
def decorator(func):
def wrapper(*args, **kw):
print('%s %s()' % (text, func.__name__))
return func(*args, **kw)
return wrapper # 最终返回值
return decorator

这个3层嵌套的decorator用法如下:

1
2
3
@log('time_log_file')  # 等价于 now = log('time_log_file')(now)
def now():
print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))

执行结果如下:

1
2
3
4
now()
# output
time_log_file now()
2020-04-17 13:09:07

理解 @

和两层嵌套的decorator相比,3层嵌套的效果是这样的:

1
>>> now = log('execute')(now)

我们来剖析上面的语句(装饰器调用关系): 1、首先执行 log('execute'),调用 log 生成真正的装饰器 decorator,参数是 text。 2、再调用返回的decorator函数,参数是 now 函数。 3、最终会去执行,decorator 函数中的 wrapper 函数,返回值最终是wrapper函数。

装饰器的累加

当一个被装饰的对象同时叠加多个装饰器时。 装饰器的加载顺序是:自下而上,也可以理解为由外到内。 装饰器内wrapper函数的执行顺序是:自上而下。

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
def dec_1(func):
def wrapper(*args, **kwargs):
print("dec_1")
return func(*args, **kwargs)
return wrapper

def dec_2(func):
def wrapper(*args, **kwargs):
print("dec_2")
return func(*args, **kwargs)
return wrapper

def dec_3(func):
def wrapper(*args, **kwargs):
print("dec_3")
return func(*args, **kwargs)
return wrapper

@dec_1
@dec_2
@dec_3
def fun():
# do something
pass

fun()

# output
dec_1
dec_2
dec_3

最后一步

以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__等属性,但你去看经过decorator装饰之后的函数,它们的__name__已经从原来的'now'变成了'wrapper'

1
2
3
4
print(now.__name__)

# output
wrapper

这是因为返回的那个wrapper()函数名字就是'wrapper'

所以,需要把原始函数的__name__等属性复制到wrapper()函数中,否则,有些依赖函数签名的代码执行就会出错。

不需要编写wrapper.__name__ = func.__name__这样的代码,Python内置的functools.wraps就是干这个事的,所以,一个完整的decorator的写法如下:

1
2
3
4
5
6
7
8
9
10
import functools

def log(text):
def decorator(func):
@functools.wraps(func) # 在装饰器里面用装饰器
def wrapper(*args, **kw):
print('%s %s()' % (text, func.__name__))
return func(*args, **kw)
return wrapper # 注意这个return wrapper对齐上面的def wrapper
return decorator # 注意这个return decorator对齐上面的def decorator

import functools是导入functools模块。现在,只需记住在定义wrapper()的前面加上@functools.wraps(func)即可。

Python中的模块,为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Python中,一个.py文件就称之为一个模块(Module)。

1
2
3
4
import time
@log("真棒")
def now():
print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))

调用

1
2
3
4
5
now()

# output
真棒 now()
2020-04-17 13:26:50

总结

在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。

decorator可以增强函数的功能,定义起来虽然有点复杂,但使用起来非常灵活和方便。

面试真题

请设计一个decorator,它可作用于任何函数上,并打印该函数的执行时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import time
import functools

def log(func):
functools.wraps(functools)
def wrapper(*args, **kw):
start = time.time()
res = func(*args, **kw) # 注意此处,并没有直接ruturn,而是保存了res的结果。
end = time.time()
print("耗时:%s 秒" % str(end-start))
return res # 在这个位置return了
return wrapper

@log
def now():
time.sleep(3)
print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))

now()

模拟一个登陆验证的装饰器?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import functools

def auth_func(username, password):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kw):
# select password from user_table where username=username
check_password = "123"
if check_password and check_password == password:
print("欢迎登录")
res = func(*args, **kw)
return res
else:
print("账号或密码错误")
return wrapper # 注意这个return wrapper对齐上面的def wrapper
return decorator # 注意这个return decorator对齐上面的def decorator

@auth_func("Tom", "123")
def index():
print("欢迎来到主页")

index()

实现一个@retry(times)装饰器,用来装饰一个函数,当被装饰的函数抛出异常时,会重新调用它,最多调用n次。

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
import time
import functools
def retry(times=10, traced_exceptions=None, reraised_exception=None):
'''设计一个装饰器函数 retry,当被装饰的函数调用抛出指定的异常时,
函数会被重新调用,直到达到指定的最大调用次数才重新抛出指定的异常。
traced_exceptions 为监控的异常,可以为 None(默认)、异常类、或者一个异常类的列表。
traced_exceptions 如果为 None,则监控所有的异常;如果指定了异常类,则若函数调用抛出指定的异常时,重新调用函数,直至成功返回结果
或者达到最大尝试次数,此时重新抛出原异常(reraised_exception 的值为 None)
,或者抛出由 reraised_exception 指定的异常。
'''
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
num = times
need_raisse = False
while True:
try:
return func(*args, **kwargs)
except Exception as e:

# 说明要捕捉所有异常,直接 pass
if traced_exceptions is None:
num -=1

# traced_exceptions只有一种,如果指定了捕捉的异常类,则执行专门处理指定类型异常的代码
elif isinstance(e, traced_exceptions):
num -= 1
# ... 写一些专门处理指定类型异常的代码

# traced_exceptions是一个list,如果指定了捕捉异常类的列表,则执行专门处理指定类型异常的代码
elif type(traced_exceptions) == list and type(e) in traced_exceptions:
num -= 1
# ... 写一些专门处理指定类型异常的代码

# 非捕捉的异常类 需要抛出异常
else:
need_raisse = True

# 重试次数完毕或非捕捉的异常类
if num == 0 or need_raisse:
# reraised_exception 为 None 则抛出原来的异常,否则只抛出指定的异常
if reraised_exception is None or type(e) == reraised_exception:
raise
else:
break
return wrapper
return decorator

@retry(times=3, traced_exceptions=ValueError, reraised_exception=NameError)
def func(num):
time.sleep(1)
print("func is called.")
if num == 0:
pass
elif num == 1:
raise NameError
elif num == 2:
raise ValueError
else:
raise Exception

for i in range(5):
func(i)

写一个decorator,能在函数调用的前后打印出'begin call''end call'的日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
import functools

def log(func):
functools.wraps(functools)
def wrapper(*args, **kw):
print("begin call")
res = func(*args, **kw) # 注意此处,并没有直接ruturn,而是保存了res的结果。
print("end call")
return res # 在这个位置return了
return wrapper

@log
def now():
time.sleep(3)
print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))

now()

写出一个既支持传参又支持不传参的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
import functools

def log(text=None):
def decorator(func):
@functools.wraps(func)
def wrapper(*arge, **kw):
if text:
print("传入参数为 {}".format(text))
else:
pass
return func(*arge, **kw)
return wrapper
return decorator

能写一个装饰器吗?用来对用户的参数进行检查,如果参数类型不匹配,就返回一个error报文,否则返回success。

待完成

装饰器的作用和本质?

作用:在代码运行期间动态增加功能

本质:装饰器本质上讲也是一个Python函数,或者可以认为是对使用这个函数的另一个函数的重定义。最本质的讲,Python里面一切皆对象,装饰器本质上也是对象。

装饰器传参?

装饰器传参

*arg 和 **karg的区别

答:

如果我们不确定往一个函数中传入多少参数,或者我们希望以元组(tuple)或者列表(list)的形式传参数的时候,我们可以使用*args(单星号)。

如果我们不知道往函数中传递多少个关键词参数或者想传入字典的值作为关键词参数的时候我们可以使用**kwargs(双星号),args、kwargs两个标识符是约定俗成的用法。

另一种答法:

当函数的参数前面有一个星号*号的时候表示这是一个可变的位置参数,两个星号**表示这是一个可变的关键词参数。** **


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!