生活不易 我用python程序员

Python装饰器解析

2018-03-19  本文已影响49人  一根薯条

和生成器一样,装饰器也是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 old 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)

可以看到,我们定义了三个函数,分别是greetingsay_hisay_hello,其中say_hisay_hello这两个函数作为一个普通的参数传递给greeting函数。greeting函数通过函数参数获得了say_hisay_hello函数的引用。因此在greeting中调用f(),其实就是调用say_hisay_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()

执行结果如下:

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 wrapper

往日志里记录函数参数

def logging(func):
       def wrapper(*args, **kwargs):
           res = func(*args, **kwargs)
           print(func.__name__, args, kwargs)
           return res
       return wrapper

函数计数器

def 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

使用inspect获取函数参数

根据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

装饰器的缺点

上一篇 下一篇

猜你喜欢

热点阅读