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装饰器解析

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