在代码运行期间动态增加功能的方式,称之为“装饰器”(Decorator)。
参考链接 廖雪峰 Pipe somenzz
背景
为何会有装饰器?
在Python里面,一切皆对象,函数也是一种对象,可以被赋值给变量,所以,通过变量也能调用该函数。
| >>> 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,可以定义如下:
| >>> def log(func): ... def wrapper(*args, **kw): ... print('call: %s()' % func.__name__) ... return func(*args, **kw) ... return wrapper
|
使用
log
是一个decorator,所以接受一个函数作为参数,并返回一个函数。我们要借助Python的@语法,把decorator置于函数的定义处:
| >>> @log ... 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()
函数的定义处,相当于执行了语句:
由于log()
是一个decorator,返回一个函数,所以,原来的now()
函数仍然存在,只是现在同名的now
变量指向了新的函数,也就是log(now)
,于是调用now()
将执行新函数log(now)
,这个新函数就是log()
函数中返回的wrapper()
。
装饰器传参
如果decorator本身需要传入参数,那就需要编写一个返回decorator的高阶函数,写出来会更复杂。比如,要自定义log的文本:
| 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用法如下:
| @log('time_log_file') def now(): print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
|
执行结果如下:
| now()
time_log_file now() 2020-04-17 13:09:07
|
理解 @
和两层嵌套的decorator相比,3层嵌套的效果是这样的:
| >>> 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(): pass
fun()
dec_1 dec_2 dec_3
|
最后一步
以上两种decorator的定义都没有问题,但还差最后一步。因为我们讲了函数也是对象,它有__name__
等属性,但你去看经过decorator装饰之后的函数,它们的__name__
已经从原来的'now'
变成了'wrapper'
:
| print(now.__name__)
wrapper
|
这是因为返回的那个wrapper()
函数名字就是'wrapper'
。
所以,需要把原始函数的__name__
等属性复制到wrapper()
函数中,否则,有些依赖函数签名的代码执行就会出错。
不需要编写wrapper.__name__ = func.__name__
这样的代码,Python内置的functools.wraps
就是干这个事的,所以,一个完整的decorator的写法如下:
| 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 decorator
|
import functools
是导入functools
模块。现在,只需记住在定义wrapper()
的前面加上@functools.wraps(func)
即可。
Python中的模块,为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Python中,一个.py文件就称之为一个模块(Module)。
| import time @log("真棒") def now(): print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
|
调用
| now()
真棒 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) end = time.time() print("耗时:%s 秒" % str(end-start)) return res 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): check_password = "123" if check_password and check_password == password: print("欢迎登录") res = func(*args, **kw) return res else: print("账号或密码错误") return wrapper return 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:
if traced_exceptions is None: num -=1 elif isinstance(e, traced_exceptions): num -= 1 elif type(traced_exceptions) == list and type(e) in traced_exceptions: num -= 1 else: need_raisse = True if num == 0 or need_raisse: 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) print("end call") return res return wrapper
@log def now(): time.sleep(3) print(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
now()
|
写出一个既支持传参又支持不传参的装饰器
| 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两个标识符是约定俗成的用法。
另一种答法:
当函数的参数前面有一个星号*号的时候表示这是一个可变的位置参数,两个星号**表示这是一个可变的关键词参数。** **