装饰器一直都是Python中非常有用的一个特征,在后端开发的Django框架中,比如日志、缓存等等的任务中都会用到。
函数 -> 装饰器
函数核心
def get_message(message):
return 'Got a message:'+message
def root_call(func,message):
return (func(message))
print(root_call(get_message,'hello world'))
在这个例子中,我们把get_message当做变量传给root_call函数。
当然,我们也可以使用函数嵌套,如下:
def func(message):
def get_message(message):
print('Got a message : {}'.format(message))
return get_message(message)
func('hello world')
# 输出
Got a message : hello world
除了函数嵌套,闭包也可以:
def func():
def get_message(message):
return ('Got a message : {}'.format(message))
return get_message
send_message = func()
send_message('hello world!')
这里,函数func()的返回值是内函数的对象get_message()本身,之后我们将get_message()实例化,再调用send_message('hello world !')输出的得到Got a message : hello world!
装饰器
闭包有一个最大的用处就是用来实现闭包,我们先看一个简单的例子:
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper
def greet():
print('hello world')
greet = my_decorator(greet)
greet()
# 输出
wrapper of decorator
hello world
在上述代码中,greet指向了内部函数wrapper(),而内部函数又会调用print和原函数greet(),然后先打印wrapper of decorator,再打印hello world。
当然,我们还可以用Python中的语法糖方法,实现更简单、更优雅,更更更Pythonic的用法:
def my_decorator(func):
def wrapper():
print('wrapper of decorator')
func()
return wrapper
@my_decorator
def greet():
print('hello world')
greet()
# 输出
wrapper of decorator
hello world
这里的@用法,我们称为语法糖,@my_decorator相当于greet = my_decorator(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
但是,假如我的另一个函数有两个参数,那么我该怎么办呢?
通常情况下,我们都会使用
*args
和**kwargs
,作为装饰器内部函数wrapper()
的参数。*args
和**kwargs
表示可变参数和可变关键字参数,当我们传入列表和元组时使用*args
,当我们使用字典传入参数时使用**kwargs
。
因此,当不确定参数的数量和类型的时候,装饰器可以写成下面的形式:
def my_decorator(func):
def wrapper(*args,**kwargs):
print('wrapper of decorator')
func(*args,**kwargs)
return wrapper
带有自定义参数的装饰器
装饰器除了能够接受原函数任意类型的参数外,还可以接收自己定义的参数。
假如我想要定义一个参数用来表示装饰器内部函数被执行的次数,那么就可以写成下面这种形式:
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)
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
你会发现,greet()函数被装饰之后,它的元信息被改变了,访问后返回的其实是wrapper()函数。
print(greet.__name__)
# 输出
wrapper
help(greet)
# 输出
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
想要解决这个问题,可以使用包functools中的内置装饰器@functools.wraps
,它可以帮助保留原函数的元信息(也就是将原函数的元信息,拷贝到对应的装饰器函数里)
import functools
def repeat(num):
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args,**kwargs):
for i in range(num):
print('wrapper of decorator!')
func(*args,**kwargs)
return wrapper
return my_decorator
@repeat(4)
def greet(message):
print(message)
print(greet.__name__)
# 输出
greet
help(greet)
# 输出
Help on function greet in module __main__:
greet(message)
类装饰器
类装饰器主要依赖函数的__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 call is : {}'.format(self.num_calls))
return self.func(*args,**kwargs)
@Count
def example():
print('hello world')
# 相当于example = count(example)
example()
example()
# 输出
num of call is : 1
hello world
num of call is : 2
hello world
这里,我们定义了类Count,初始化时给func传入exaple()函数, __call__()
函数表示让变量num_calls自增1,然后打印调用次数和原函数。
静态装饰器
@staticmethod
静态方法,不需要表示自身对象的self和自身类的cls参数,就跟使用函数一样
class static_method_class(object):
@staticmethod
def static_method_use():
print('@staticmethod is run!')
static_method_class.static_method_use()
# 输出
@staticmethod is run!
函数的嵌套
一句话总结:装饰器函数在函数调用时从上到下执行,嵌套时从里向外执行
@decorator1
@decorator2
@decorator3
def func():
····
# 执行顺序为:
dectorator1(dectorator2(dectorator3(func)))
装饰器的用法示例
身份认证
当你访问一下网站,你不登录就可以访问,但是当你需要发表评论、点赞等操作,服务端就会查询你是否登录,必须登录才有权限执行这些操作。
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)
else:
raise Exception('Authenticate failed')
return wrapper
@authenticate
def post_comment(request,···):
···
在这个例子中,我们定义了一个装饰器@authenticate
,函数post_comment()
用来发表评论,每次调用这个函数前,都会检查这个用户是否登录,如果是登录状态,则允许评论;如果没有登录,则不允许评论。
日志记录
日志记录也是一种装饰器的常用案例,如果要测试某些函数的执行时间,那么装饰器就是一种很常用的手段。
import time
import functools
def log_excute_time(func):
def wrapper(*args,**kwargs):
start = time.perf_counter()
res = func(*args,**kwargs)
end = time.perf_counter()
print('{} took {} s'.format(func.__name__,(end-start)*1000))
return res
return wrapper
@log_excute_time
def calculate_similarity(items):
print('run the '+items)
calculate_similarity('code !')
# 输出
run the code !
calculate_similarity took 0.0539150000000016 s
通过装饰器@log_excute_time
可以计算某个函数的运行时间,可以通过time.time()
和time.perf_counter()
计算函数运行时间,但time.perf_counter()
精度比time.time()
更高,time.time()
更适合计算日期时间。
输入合理性检查
在机器学习乃至深度学习中,对于数据集进行训练前,往往会用装饰器对输入的json进行合理性检查,这可以大大避免不正确的数据导致的梯度无法收敛等问题。
import functools
def validation_check(input):
@functools.wraps(func)
def wrapper(*args,**kwargs):
# 检查是否合法
···
@validation_check
def training(param1,param2,param3,···):
···
缓存
LRU全称为:Least recently use 最近少使用,在有限空间存储对象时,当对key访问时,将该key放到队列的最前端,这就实现了对key按照最后一次访问的时间降序排列,当向空间中增加新对象时,如果空间满了,删除队尾的对象。
缓存装饰器的用法,其实十分常见,LRU cache在Python中的实现形式是@lru_cache
,@lru_cache
会缓存进程中的参数和结果,当缓存满了之后,会删除least recently use 数据。
大公司的服务端代码中,会检查你使用的设备是iPhone还是Android,版本号是什么,将数据加入LRU cache,调用函数前都会用装饰起来包裹这些检查函数,避免反复调用,提高进程的运行效率。
from functools import lru_cache
@lru_cache
def check(param1,param2,···):# 检查设备型号
···
总结
装饰器是通过闭包来实现的,装饰器函数的特点是:可以在不更改原函数的基础上,为原函数添加一些功能
Q.E.D.