和生成器一样,装饰器也是Python独有的概念,面试中非常容易被考察到。这篇文章从以下角度尝试解析Python装饰器:
Python从2.4版本引入了装饰器的概念,所谓装饰器 是一种修改函数的快捷方式。 适当使用装饰器能够有效提高代码可读性和可维护性。装饰器本质上就是一个函数,这个函数接收被装饰的函数 作为参数,最后返回一个被修改后的函数作为原函数的替换。
前面提到,装饰器本质是一个函数,为了理解装饰器,首先我们先来了解下Python的函数。
def say_hi():
print("hello!")
hello = say_hi
hello()import random
n = random.randint(1, 5)
if n % 2 == 0:
def display(n):
print("{0} is an even number".format(n))
else:
def display(n):
print("{0} is an odd number".format(n))
display(n)而且,函数的定义也是可以嵌套进行的,如下所示:
def outer(x, y):
def inner():
return x + y
return inner
f = outer(1, 2)
print(f())在这个例子中,当我们调用函数f时,实际引用的是inner函数。
def greeting(f):
f()
def say_hi():
print('Hi!')
def say_hello():
print("Hello!")
greeting(say_hello)
greeting(say_hi)可以看到,我们定义了三个函数,分别是 greeting、 say_hi、 say_hello,其中 say_hi和 say_hello这两个函数作为一个普通的参数传递给 greeting函数。 greeting函数通过函数参数获得了 say_hi和 say_hello函数的引用。因此在 greeting中调用 f(),其实就是调用 say_hi和 say_hello函数。
再来看一个例子:
def say_hi():
print('Hi!')
def bread(f):
def wrapper(*arg, **kwargs):
print("begin {0}".format(f.__name__))
f()
print("end {0}".format(f.__name__))
return wrapper
say_hi_copy = bread(say_hi)
say_hi_copy()注意装饰器里的 return的是 wrapper而不是 wrapper(). (wrapper是一个function object,而wapper( ) 是 result of a call) 执行结果如下:
begin say_hi
Hi!
end say_hi现在让我们引入装饰器的例子:
def bread(f):
def wrapper(*arg, **kwargs):
print("begin {0}".format(f.__name__))
f()
print("end {0}".format(f.__name__))
return wrapper
@bread
def say_hi():
print('Hi!')
say_hi()这段函数的输出结果和前面一样。可以看到,前面的程序显性的用了bread函数来封装 say_hi函数,而后面的装饰器通过Python语法汤来封装 say_hi函数。 在Python中, say_hi函数定义语句 前一行 的 @bread语句表示该函数用 bread装饰器。 @是装饰语法, bread是装饰器名称。
import time
def benchmark(func):
def wrapper(*args, **kwargs):
t = time.clock()
res = func(*args, **kwargs)
print(func.__name__, time.clock() -t)
return res
return wrapperdef logging(func):
def wrapper(*args, **kwargs):
res = func(*args, **kwargs)
print(func.__name__, args, kwargs)
return res
return wrapperdef counter(func):
def wrapper(*args, **kwargs):
wrapper.count= wrapper.count+ 1
res = func(*args, **kwargs)
print("{0} has been used: {1}x".format(func.__name__, wrapper.count))
return res
wrapper.count= 0
return wrapper装饰器接受一个函数作为参数,并将一个做了修改后的函数进行替换。因此,默认情况下,获取一个被装饰器修改后的函数的属性将不能获取到正确的属性信息。例如:对于一个函数,我们可以通过 __name__属性得到函数的名字。通过 __doc__属性得到函数的帮助信息。但是,一个被装饰器装饰过的函数。默认情况下,我们通过 __doc__和 __name__获取到的是装饰器中嵌套函数的信息。如下所示:
def benchmark(func):
def wrapper(*args, **kwargs):
t = time.clock()
res = func(*args, **kwargs)
print(func.__name__, time.clock() - t)
return res
return wrapper
def mul(a, b):
"""Calculate the product of two numbers"""
return a * b
@benchmark
def add(a, b):
"""Calculate the sum of two numbers"""
return a + b
print(mul.__name__)
print(mul.__doc__)
print(add.__name__)
print(add.__doc__)返回结果如下:
mul
Calculate the product of two numbers
wrapper
None可以看到,被装饰器修饰后,函数无法获取到正确的帮助信息。 这个问题的解决方法是 使用标准库 functools模块中的 wraps装饰器。这个装饰器的作用是复制函数属性到被装饰的函数。使用方法如下:
import functools
def benchmark(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
t = time.clock()
res = func(*args, **kwargs)
print(func.__name__, time.clock() - t)
return res
return wrapper根据Python函数中的参数匹配原则,关键字参数会根据名字进行匹配,位置参数将根据所在位置进行匹配。但是如果在装饰器修饰后的函数无法准确获取到这两种参数。举个例子:
def check_is_admin(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
if kwargs.get('username') != 'admin':
raise Exception("This user is not allowed to get food")
return f(*args, **kwargs)
return wrapper在这个装饰器中,我们直接从kwargs中获取username这个键的值,获取完以后与admin进行比较,如下所示:
if kwargs.get('username') != 'admin':
raise Exception("This user is not allowed to get food")如果我们用装饰器修饰函数,而且这样传参:
func('admin', element=2)这样调用会出错,问题出现在装饰器的参数传递中。如果用户使用关键字参数的形式传递 username,那么 username变量以及值将位于 arg中。这就存在一个问题,从Python的语法中讲,用户使用位置参数或者关键字参数都是合法的,如何才能正确判断用户是否具有相应的权限呢? 这个问题是由于我们无法控制用户使用位置参数还是关键字参数。
对于这种情况,比较好的做法是使用 inspect标准库。这个库提供了很多有用的函数来获取活跃对象的信息。其中 getcallargs用来获取函数的参数信息。 getcallargs会返回一个字典,该字典保存了函数的所有参数,包括关键字参数和位置参数。也就是说 getcallargs能够根据函数的定义和传递给函数的参数,推测出哪一个值传递给函数的哪一个参数。因此,我们在检查 username参数的取值是否是 admin之前,可以先使用 getcallargs获取函数的所有参数,然后从 getcallargs返回的字典里获取 username的取值。这样就可以解决问题了。下面是示例代码:
import inspect
def check_is_admin(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
func_args= inspect.getcallargs(f, *args, **kwargs)
if func_args.get('username') != 'admin':
raise Exception("This user is not allowed to get food")
return f(*args, **kwargs)
return wrapper当多个装饰器装饰一个函数的时候,装饰器起作用的顺序是:先执行离函数最近的装饰器。(可以理解为多个函数的嵌套)
有时候,装饰器本身也是需要传递参数的,如果遇到这种情况,只需要再嵌套一层函数。下面是一个带有参数的装饰器:
def timeout(seconds, error_message= 'Function call timed out'):
def decorated(func):
def _handle_timeout(signum, frame):
raise TimeoutError(error_message)
def wrapper(*args, **kwargs):
signal.signal(signal.SIGALRM, _handle_timeout)
signal.alarm(seconds)
try:
result = func(*args, **kwargs)
finally:
signal.alarm(0)
return result
return functools.wraps(func)(wrapper)
return decorated