10. 强大的装饰器
装饰器的概念:
Decorators is to modify the behavior of the function through a wrapper so we don’t have to actually modify the function
即所谓的装饰器,是通过装饰器函数来修改原函数的一些功能,使得不需要对原函数进行修改。
10.1 函数装饰器
10.1.1 简单函数装饰器
对于如下代码:
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper
greet = my_decorator(greet)
greet()
# 输出
wrapper of decorator
hello world
上面代码中,变量greet指向了内部函数wrapper(),而内部函数wrapper()中又会调用原函数greet();
上面的代码可使用装饰器的写法进行简化:
def my_decorator(func):
def wrapper(message):
print('wrapper of decorator')
func(message)
return wrapper
@my_decorator
def greet(message): #这里函数带有参数,也可不带参数,属于更简单的情况
print(message)
greet('hello world')
# 输出
wrapper of decorator
hello world
其中,@my_decorator+原函数greet():就等价于greet=my_decorator(greet)。
我是这么理解的:
- 对于@my_decorator+原函数 greet():,@my_decorator将原函数作为其参数传入,也就是形参func;
- 对于原函数greet(arg),可以说它使用了装饰器@my_decorator,使自己成为了装饰器的参数,并将自己的参数arg作为装饰器内部函数wrapper()的参数。特别地,若greet()原函数有多个参数,则使用*args,**kwargs作为装饰器内部函数wrapper()的参数,表示接受任意数量和类型的参数
def my_decorator(func):
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
10.1.2 带有参数的函数装饰器
装饰器除了可以接受原函数任意数量和类型的参数之外,我们还可通过装饰器本身传入参数。
比如,我们通过传入自定义参数,控制装饰器内部函数的执行次数,可写为:
def repeat(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
for i in range(num):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
return my_decorator
@repeat(4) # 装饰器传入参数4
def greet(message):
print(message)
greet('hello world')
# 输出:
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
10.1.3 原函数在使用装饰器后的变化
在使用装饰器之后,我们可以这样查看原函数greet()的信息:
greet.__name__
## 输出
'wrapper'
help(greet)
# 输出
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
可见原函数被装饰之后,元信息发生了变化,原函数被装饰器的内部函数wrapper()替代了。
为了解决这个问题,我们可以在装饰器的定义中使用内置的装饰器@functools.wrap,它会把原函数的元信息拷贝到对应的装饰器函数里:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('wrapper of decorator')
func(*args, **kwargs)
return wrapper
@my_decorator
def greet(message):
print(message)
greet.__name__
# 输出
'greet'
10.2 类装饰器
类也可以作为装饰器,类装饰器主要依赖于函数__call__(),每当调用一个类的示例时,函数__call__()就会被执行一次。
如以下代码所示:
class Count:
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print('num of calls is: {}'.format(self.num_calls))
return self.func(*args, **kwargs)
@Count
def example():
print("hello world")
example()
# 输出
num of calls is: 1
hello world
example()
# 输出
num of calls is: 2
hello world
...
10.3 装饰器的嵌套
之前讲的情况都是一个装饰器的情况,实际上python也支持多个装饰器,比如:
@decorator1
@decorator2
@decorator3
def func():
...
执行顺序为由里到外,等效于如下代码:
decorator1(decorator2(decorator3(func)))
下面改写上面hello world的例子,代码可写为:
import functools
def my_decorator1(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('execute decorator1')
func(*args, **kwargs)
return wrapper
def my_decorator2(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('execute decorator2')
func(*args, **kwargs)
return wrapper
@my_decorator1
@my_decorator2
def greet(message):
print(message)
greet('hello world')
# 输出
execute decorator1
execute decorator2
hello world
10.4 装饰器用法示例
10.4.1 身份认证
虎扑上面的帖子可以在未登录时进行浏览,但是要评论的话就会激发身份认证来确认是否处于登录状态才有评论权限。
import functools
def authenticate(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
request = args[0]
if check_user_logged_in(request): # 如果用户处于登录状态
return func(*args, **kwargs) # 执行原函数post_comment()
else:
raise Exception('Authentication failed')
return wrapper
@authenticate
def post_comment(*args, **kwargs):
...
装饰器@authenticate修饰了函数post_comment(),这使得每次调用post_comment()前,都会去检查用户是否处于登录状态。
10.4.2 日志记录函数运行时间
在实际工作中,如果你怀疑某些函数的运行时间过长,导致整个系统的latency增加,所以想在线上测试函数的执行时间,那么,装饰器是一种很常用的手段。
import time
import functools
def log_execution_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
res = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {(end-start)*1000} ms")
return res
return wrapper
@log_execution_time
def save_comment(*args, **kwargs):
...
这里,装饰器@log_execution_time记录了函数的执行时间,并返回其结果。如果想计算任何函数的执行时间,只需要在这个函数上方加上@log_execution_time即可。
10.4.3 输入合理性检查
在大型公司的机器学习框架中,调用机器集群进行模型训练前,往往会用装饰器对其输入(往往是很长的json文件)进行合理性检查。这样可以大大避免由于输入不正确而对机器造成的巨大的开销。
大概格式为:
import functools
def validation_check(input):
@functools.wraps(input)
def wrapper(*args, **kwargs):
# 检查输入是否合理
...
return wrapper
@validation_check
def netural_network_training(param1, param2, ...):
...
在实际工作中,很多情况都会出现输入不合理现象。因为往往有时需要输入的文件有成千上万行,很多时候确实很难发现。如果没有输入合理性检查,很容易出现模型训练了几个小时却最终报错输入参数不对的情形,极大地降低了开发效率,对机器资源造成了极大的浪费。
10.4.4 缓存
python就有一个内置的缓存机制LRU cache,表现形式为@lru_cache,它会缓存进程中的函数参数和结果,当缓存满了之后,会删除least recently used 的数据。
正确地使用缓存装饰器,往往能极大地提高程序运行效率。
比如,大型公司的服务器端的代码往往存在很多关于设备的检查,比如该设备是ios还是安卓,版本是多少,其中的原因,就是一些新的feature,往往只有在某些特定的手机系统或版本上才有。
这样一来,我们可以使用缓存装饰器来包裹这样的函数,使得其不会被反复调用进行查询,提高了效率:
@lru_cache
def check(param1, param2, ...) # 检查用户设备类型,版本号等等
...