Python装饰器解析

作者: 一根薯条 | 来源:发表于2018-03-19 07:27 被阅读49次

和生成器一样,装饰器也是Python独有的概念,面试中非常容易被考察到。这篇文章从以下角度尝试解析Python装饰器:

  • 装饰器概念
  • 理解装饰器所需的函数基础
  • 装饰器使用场景
  • 使用装饰器需要注意的地方
  • 装饰器的缺点

装饰器概念

Python从2.4版本引入了装饰器的概念,所谓装饰器 是一种修改函数的快捷方式。 适当使用装饰器能够有效提高代码可读性和可维护性。装饰器本质上就是一个函数,这个函数接收被装饰的函数 作为参数,最后返回一个被修改后的函数作为原函数的替换。

前面提到,装饰器本质是一个函数,为了理解装饰器,首先我们先来了解下Python的函数。

理解装饰器所需的函数基础

  • 函数对象
    在Python中,def语句定义了一个函数对象,并将其赋值给函数名。This is to say,函数名只是一个变量,这个变量引用了函数对象。因此,我们可以将函数赋值给另外的变量名,然后通过新的变量名调用函数。如图所示:
def say_hi():
    print("hello!")
hello = say_hi
hello()
  • 嵌套函数
    在Python中,def是一个实时执行的语句,当它运行的时候会创建一个新的函数对象,并将其赋值给一个变量名。这里所说的变量名就是函数的名称。因为def是一个语句,因此,函数定义可以出现在其他语句中:
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

使用装饰器需要注意的地方

  • 函数的属性变化
  • 使用inspect获取函数参数
  • 多个装饰器的调用顺序
  • 给装饰器传递参数

装饰器接受一个函数作为参数,并将一个做了修改后的函数进行替换。因此,默认情况下,获取一个被装饰器修改后的函数的属性将不能获取到正确的属性信息。例如:对于一个函数,我们可以通过__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

装饰器的缺点

  • 必须运行在Python2.4以上的版本
  • 装饰器降低函数调用的速度
  • 有时候使程序更难debug

相关文章

  • Python装饰器解析

    和生成器一样,装饰器也是Python独有的概念,面试中非常容易被考察到。这篇文章从以下角度尝试解析Python装饰...

  • 装饰器模式

    介绍 在python装饰器学习 这篇文章中,介绍了python 中的装饰器,python内置了对装饰器的支持。面向...

  • Python 装饰器深度解析

    参考:https://zhuanlan.zhihu.com/p/45458873 假设现在有一个 add 求和函数...

  • python中的装饰器

    python装饰器详解 Python装饰器学习(九步入门) 装饰器(decorator) 就是一个包装机(wrap...

  • [译] Python装饰器Part II:装饰器参数

    这是Python装饰器讲解的第二部分,上一篇:Python装饰器Part I:装饰器简介 回顾:不带参数的装饰器 ...

  • Python中的装饰器

    Python中的装饰器 不带参数的装饰器 带参数的装饰器 类装饰器 functools.wraps 使用装饰器极大...

  • Python进阶——面向对象

    1. Python中的@property   @property是python自带的装饰器,装饰器(decorat...

  • Python 装饰器填坑指南 | 最常见的报错信息、原因和解决方

    Python 装饰器简介装饰器(Decorator)是 Python 非常实用的一个语法糖功能。装饰器本质是一种返...

  • Python装饰器

    Python装饰器 一、函数装饰器 1.无参装饰器 示例:日志记录装饰器 2.带参装饰器 示例: 二、类装饰器 示例:

  • python3基础---详解装饰器

    1、装饰器原理 2、装饰器语法 3、装饰器执行的时间 装饰器在Python解释器执行的时候,就会进行自动装饰,并不...

网友评论

    本文标题:Python装饰器解析

    本文链接:https://www.haomeiwen.com/subject/hmkuqftx.html