一、函数的核心点
在聊装饰器之前,我们先“约法三章”,如果这“三章”都不承认,那我们的装饰器也就没得聊了,哪三章呢?
- 在Python中,函数是一等公民,函数也是对象,我们可以把函数赋予变量。
比如下面的代码:
# -*- coding=utf-8 -*-
def func(message):
print('Got a message: {}'.format(message))
if __name__ == "__main__":
send_message = func
send_message('Hello World')
- 我们可以把函数当作参数,传入另一个函数中。
# -*- coding=utf-8 -*-
def func(message):
print('Got a message: {}'.format(message))
def root_call(func, message):
func(message)
if __name__ == "__main__":
root_call(func, "Hellow World")
- 我们可以在函数里定义函数,也就是函数的嵌套。
# -*- coding=utf-8 -*-
def func(message):
def get_message(message):
print('Got a message: {}'.format(message))
return get_message(message)
if __name__ == "__main__":
func('hello world')
- 要知道,函数的返回值也可以是函数对象(闭包).
比如下面这个例子:
def func_closure():
def get_message(message):
print('Got a message: {}'.format(message))
return get_message
send_message = func_closure()
send_message('hello world')
二、一个简单的装饰器
我们先从一个简单的例子开始,这个例子是建立在充分理解“我们的约法三章”基础之上的。
# -*- coding=utf-8 -*-
import time
def time_decorator(func):
def wrapper():
start_time = time.time()
func()
print(time.time()-start_time)
return wrapper
def print_hello():
print("Hello")
time.sleep(2)
if __name__ == "__main__":
greet = time_decorator(print_hello)
greet()
在这段代码中,变量greet
指向了内部函数wrapper
,而内部函数又会调用原函数print_hello
。最后执行greet
时,会计算执行greet
的时间。
这里的函数time_decorator
便是一个简单的装饰器,它可用于计算某个函数的执行时间
事实上,上述代码有一个更优雅的方式:
# -*- coding=utf-8 -*-
import time
def time_decorator(func):
def wrapper():
start_time = time.time()
func()
print(time.time()-start_time)
return wrapper
@time_decorator
def print_hello():
print("Hello")
time.sleep(2)
if __name__ == "__main__":
print_hello()
这里的@
,我们被称之为语法糖,@time_decorator
就相当于前面的greet = time_decorator(print_hello)
语句,只不过是更加简洁。因此如果我们的程序中有其它函数需要做类似的装饰,只需要在它们的上方加上@decorator
就可以了,这样就大大提高了函数的重复利用和程序的可读性。
三、带有参数的装饰器
在现实的很多情况中,函数都是带有参数的。例如:
def print_message(message):
print(message)
如果我们想给上述类型的函数添加装饰器应该如何做呢?
简单的方式便是在对应的装饰器函数wrapper上,加上相应的参数:
def time_decorator(func):
def wrapper(message):
start_time = time.time()
func(message)
print(time.time()-start_time)
return wrapper
完整示例如下:
# -*- coding=utf-8 -*-
import time
def time_decorator(func):
def wrapper(message):
start_time = time.time()
func(message)
print(time.time()-start_time)
return wrapper
@time_decorator
def print_hello():
print("Hello")
time.sleep(2)
@time_decorator
def print_message(message):
print(message)
if __name__ == "__main__":
print_message("Hello World")
不过,新的问题来了。如果存在另外一个函数,也需要使用time_decorator()装饰器,但是这个新的函数有两个参数,又该怎么办呢?
事实上,通常情况下,我们会把*args
和**kwargs
作为装饰器内部函数wrapper()的参数。*args
和**kwargs
,表示接受任意数量和类型的参数,因此装饰器就可以写成下面的形式:
def time_decorator(func):
def wrapper(*args, **kwargs):
start_time = time.time()
func(*args, **kwargs)
print(time.time()-start_time)
return wrapper
四、带有自定义参数的装饰器
其实,装饰器还有更大程度的灵活性。装饰器可以接受原函数任意类型和数量的参数,除此之外,它还可以接受自己定义的参数。
比如我们想要定义一个参数,来表示装饰器内部函数被执行的次数,那么就可以写成下面的形式:
def repeat(num):
def my_decorator(func):
def wrapper(*args, **kwargs):
for i in range(num):
func(*args, **kwargs)
return wrapper
return my_decorator
@repeat(4)
def print_name_message(name, message):
print(name)
print(message)
if __name__ == "__main__":
print_name_message('leo', 'message')
五、原函数还是原函数吗?
我们试着打印出greet()函数的一些元信息:
print(print_name_message.__name__)
help(print_name_message)
# 输出
wrapper
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
我们发现,函数被装饰以后,它的元信息变了。元信息告诉我们『它不再是以前的那个函数,而是被wrapper函数取代了』。
为了解决这个问题,我们通常使用内置的装饰器@functools.wrap,它会帮助保留原函数的元信息(也就是将原函数的元信息,拷贝到对应的装饰器函数里)
import functools
def repeat(num):
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i in range(num):
func(*args, **kwargs)
return wrapper
return my_decorator
@repeat(4)
def print_name_message(name, message):
print(name)
print(message)
if __name__ == "__main__":
print_name_message('leo', 'message')
六、类装饰器
上述介绍了函数作为装饰器的方法,实际上,类也可以作为装饰器。类装饰器主要依赖于函数call(),每当调用一个类的示例时,函数call()就会被执行一次。
class Count(object):
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 wolrd")
if __name__ == "__main__":
example()
example()
这里,我们定义了类Count,初始化时传入原函数func(),而call()函数表示让变量num_calls自增1,然后打印,并且调用原函数。
七、装饰器的嵌套
Python支持多个装饰器,比如下面的形式:
@decorator1
@decorator2
@decorator3
def func():
...
它的执行顺序从里到外,所以上面的语句也等效于下面这行代码:
decorator1(decorator2(decorator3(func)))
八、一个典型的装饰器实例用法
在实际过程中,如果我们怀疑某些函数的耗时时间过长,导致整个系统的latency(延迟)增加,所以想在线上测试某些函数的执行时间,那么装饰器就是一种很常用的手段。
# -*- coding=utf-8 -*-
import time
import functools
def log_execution_time(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
end = time.time()
print('{} took {} ms'.format(func.__name__, (end - start) * 1000))
return res
return wrapper
@log_execution_time
def sleep():
time.sleep(5)
if __name__ == "__main__":
sleep()