本文主要介绍装饰器的使用,难点在于理解装饰器的工作原理。我尽量多举例子,少废话。看完本文之后,大家应该能够模仿文中范例,独立写出一个装饰器。
软件开发领域中最经典的口头禅就是“don’t repeat yourself”。 也就是说,任何时候当你的程序中存在高度重复(或者是通过剪切复制)的代码时,都应该想想是否有更好的解决方案。装饰器就是python中一种可以有效避免重复的工具。相对于其它方式,装饰器语法简单,代码可读性高。因此,装饰器在Python项目中有广泛的应用。
python官方文档写到:“装饰器是一个函数,其主要用途是包装另一个函数或类。这种包装的首要目的是透明地修改或增强被包装对象的行为。”
简单点说,装饰器是一种特殊的函数,它接受的参数中必须要有一个函数,它的返回值也必须是函数。是的,你没看错,在python中函数也可以是参数。还记得聪哥之前说过,在python中,一切都是对象。从这个角度看,函数和列表元组等没有本质上的区别。
# 给函数countdown加上装饰器timethis
@timethis
def countdown(n):
pass
# 等价于先定义函数countdown,然后用函数timethis处理它
def countdown(n):
pass
countdown = timethis(countdown)
装饰器本质上是函数,因而定义装饰器类似于定义一个函数。定义装饰器需要用到functools,这是一个操作函数的模块,其中的wraps方法就是用来定义装饰器的。下面是一个定义装饰器的示例:
# 引入time模块,它是一个时间显示和处理模块
import time
# 引入wraps,利用它可以制作装饰器
from functools import wraps
# 定义装饰器timethis,该装饰器的功能是记录函数运行时间
def timethis(func):
'''
Decorator that reports the execution time.
'''
@wraps(func)
def wrapper(*args, **kwargs):
# 函数开始运行时间
start = time.time()
result = func(*args, **kwargs)
# 函数运行结束时间
end = time.time()
# __name__属性指的是函数的名称
print(func.__name__, end-start)
return result
return wrapper
装饰器的使用方法很简单,只要在定义的函数之前加上装饰器的名称即可。
# 使用上面定义的timethis装饰器来修饰函数countdown
@timethis
def countdown(n):
'''
Counts down
'''
while n > 0:
n -= 1
# countdown函数的作用就是将一个数不断减一,最终减至0或负数
>>> countdown(100000)
countdown 0.008917808532714844
>>> countdown(10000000)
countdown 0.87188299392912
不难发现,函数countdown的每一次运行时间都被打印到了控制台,这个功能就是timethis装饰器提供的。
通过上面的例子,可以归纳出装饰定义的一般规则:
from functools import wraps
def 装饰器名(func):
@wraps(func)
def wrapper(参数):
代码块
return wrapper
其中:装饰器名称是自定义的,func是需要用装饰器修饰的函数名称,wrapper是被装饰器修饰后返回的新函数名称。func和wrapper都是自定义的,换成其它名称也不影响使用。代码块实现的装饰器的功能。
前面,我们定义装饰器的时候,总是引入wraps装饰器,那么使用这个装饰器有什么作用呢?请看下面的代码。
# 针对之前定义的countdown函数
>>> countdown.__name__
'countdown'
>>> countdown.__doc__
'\n\tCounts down\n\t'
>>> countdown.__annotations__
{'n': <class 'int'>}
如果我们定义timethis装饰器时不使用wraps装饰器修饰wrapper,此时countdown的属性:
>>> countdown.__name__
'wrapper'
>>> countdown.__doc__
>>> countdown.__annotations__
{}
很显然,__doc__
和 __annotations
属性值都丢失了。所以,wraps装饰器是用来保持被修饰函数的元信息的。
有的时候,我们不希望每个函数的运行时间都被记录下来,这个时候可以用一个参数来控制装饰器的执行。这个参数很显然是加在装饰器上的,示例代码如下:
import time
from functools import wraps
# 定义装饰器timethis,该装饰器的功能是记录函数运行时间
def timethis(arg = True):
'''
Decorator that reports the execution time.
'''
# 嵌套一层_timethis函数
if arg:
# 当装饰器传入的参数是True的时候记录函数运行时间
def _timethis(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)
return result
return wrapper
else:
# False的时候,直接返回原函数
def _timethis(func):
return func
return _timethis
上面定义了两个_timethis
装饰器,timethis
装饰器的参数决定了哪一种_timethis
装饰器被返回。
类似于形容词修饰名词,装饰器也可以修饰函数。一个名词可以有多个形容词修饰,那么一个函数是否可以用多有装饰器修饰?答案是可以的。那么问题来了,如果有多个装饰器同时修饰一个函数,到底谁先修饰?换句话说,哪一个装饰器的代码优先被执行?
请看下面的两个装饰器:
import time
from functools import wraps
def decorator_1(func):
print("Enter into decorator_1")
@wraps(func)
def wrapper(*args, **kwargs):
print("Enter into decorator_1_wrapper")
func(*args, **kwargs)
return wrapper
def decorator_2(func):
print("Enter into decorator_2")
@wraps(func)
def wrapper(*args, **kwargs):
print("Enter into decorator_2_wrapper")
func(*args, **kwargs)
return wrapper
函数hello被这样两个装饰器修饰,之后调用hello函数:
>>> @decorator_1
··· @decorator_2
··· def hello(name):
··· '''
··· Welcome somebody
··· '''
··· print("Hello, %s" % name)
···
>>> hello("jack")
Enter into decorator_2
Enter into decorator_1
Enter into decorator_1_wrapper
Enter into decorator_2_wrapper
Hello, jack
发现装饰器的调用顺序是:先调用靠近函数名的装饰器,后调用远离函数名的装饰器,等到运行装饰器中的代码时,顺序恰好相反。最后实现函数本身的功能。
-
引入日志
-
函数执行时间统计
-
预处理
-
后处理
-
权限校验
-
缓存
- 装饰器时用来修饰函数的,它可以实现优雅地复用代码,简化编程
- 装饰器的定义过程实质上是:引入旧函数生成新函数的过程
- wraps装饰器可以保留被修饰函数的元信息
- 装饰器可以添加参数,实现更高级的功能
- 定义多个装饰器,需要注意它们的调用顺序
定义一个装饰器,打印出函数所有输入的参数和返回值
from functools import wraps
def print__(func):
'''
Decorator that print the input and output.
'''
@wraps(func)
def wrapper(*args, **kwargs):
print(*args, **kwargs)
result = func(*args, **kwargs)
print(result)
return result
return wrapper