Zexian Li

一文看懂Python装饰器(Decorators)

2021-04-12 · 7 min read
Python

在代码中接触了许多有关Python装饰器的内容,简单写一篇博客来记录一下,以作归纳总结。
“装饰器的强大在于它能够在不修改原有业务逻辑的情况下对代码进行扩展,权限校验、用户认证、日志记录、性能测试、事务处理、缓存等都是装饰器的绝佳应用场景,它能够最大程度地对代码进行复用。”[1]
装饰器最简单的功能就是做日志记录和时间统计。下面以日志记录的代码为例引入装饰器的概念,其中在函数定义上一行中的@便是Python对应装饰器的语法糖。

def my_logging(func):

    def wrapper():

        print("{} begins.".format(func.__name__))
        return func()

    return wrapper


@my_logging     
def my_func():

    print("Welcome to my blog.")


if __name__ == "__main__":
    # The decaorator replace 'my_func = my_logging(my_func)'
    my_func()

    # --------Result---------
    # my_func begins.
    # Welcome to my blog.

只要能正确理解Python中函数名的实际内涵、函数名作为参数传递的过程,便能很清晰看懂上述示例。请务必理解清楚再继续阅读,下面深入探讨较复杂情况下装饰器使用的示例都是对上例的扩展:

(1)被装饰函数涉及多个参数

如果上例中的my_func函数带有参数,我们只需要在wrapper函数中加上对应参数即可正常调用;如果my_func函数涉及若干个参数,我们可以使用*args和**kargs传递参数(可以这样理解,在函数传递参数时使用*args作为形参,在使用参数时直接用args作为实参)。后一种情况的示例代码如下:

def my_logging(func):

    def wrapper(a, *args, **kargs):

        print("{} begins.".format(func.__name__))
        return func(a, *args, **kargs)

    return wrapper


@my_logging
def my_func(a, *args, **kargs):

    print("Welcome to my blog.")
    print("a = {}".format(a))
    print("I can get 'args': {}.".format(args))
    print("I can get 'kargs': {}.".format(kargs))


if __name__ == "__main__":
    my_func(1, 2, 3, 4, b=5, c=6, d=7)

    # --------Result---------
    # my_func begins.
    # Welcome to my blog.
    # a = 1
    # I can get 'args': (2, 3, 4). type: tuple
    # ps: *args: 2,3,4
    # I can get 'kargs': {'b': 5, 'c': 6, 'd': 7}. type: dict
    # ps: *kargs: b c d

可以这样近似地理解,我们在主函数处执行

my_func = my_logging(my_func)
my_func(1, 2, 3, 4, b=5, c=6, d=7)

而这些最后在my_logging中变成了执行wrapper((a, *args, **kargs))
上述代码中涉及到了**闭包(Closure)**的概念[2]。 在函数内部创建一个内嵌函数是合法的,且内嵌函数只有在外部函数的作用域内方可正常调用。如果一个外函数中定义了一个内函数,且内函数体内引用到了内函数体外、外函数内的变量,这时外函数通过return返回内函数的引用时,会把涉及到的内函数体外的变量和内函数打包成一个整体(闭包)返回,内部函数即为闭包函数,闭包函数所引用的外部定义的变量叫做自由变量。可以参照下例了解闭包的概念:

def outer(x):
    a = x

    def inner(y):
        b = y
        print(a+b)

    return inner


f1 = outer(1)            # 返回inner函数对象和外部引用变量a的闭包
f1(10)                   # 相当于inner(10),输出11

通常一个函数运行结束的时候,临时变量会被销毁,但闭包是一个特殊情况。当外函数发现自己的临时变量将来会在内函数中用到,则外函数在结束并返回内函数的同时会把外函数的临时变量同内函数绑定在一起,这保证了外函数结束后内函数的正常使用。
示例中的my_logging函数和wrapper函数正是同样的闭包形式。

(2)装饰器带参数

此处的high_level_logging函数便是带参数的装饰器,该装饰器的返回值是最初示例中那个基本的装饰器。high_level_logging可以被理解为对最基本装饰器的函数封装,或一个含有参数的闭包,其内部执行过程就像套娃一样禁止套娃🤐

import logging

def high_level_logging(level='info'):

    def my_logging(func):

        def wrapper():

            print("{} begins.".format(func.__name__))
            if level == 'warn':
                logging.warning("Attention!")
            return func()

        return wrapper

    return my_logging


@high_level_logging(level='warn')
def my_func():

    print("Welcome to my blog.")


if __name__ == "__main__":
    my_func()

    # --------Result---------
    # my_func begins.
    # WARNING:root:Attention!
    # Welcome to my blog.

当执行@high_level_logging(level='warn')时,Python发现该封装并将参数传递到装饰器的环境中。

(3)类装饰器

装饰器可以是类,使用类装饰器主要依靠类的__call__方法。简要示例如下:

class my_logging():

    def __init__(self, func):
        
        self._func = func

    def __call__(self):
        
        print("{} begins.".format(self._func.__name__))
        self._func()


@my_logging     
def my_func():

    print("Welcome to my blog.")


if __name__ == "__main__":

    my_func()
    
    # --------Result---------
    # my_func begins.
    # Welcome to my blog.

当然,类装饰器也可以带参数。
为了更直观显示属性,有时需要调用functools.wraps模块以完成原函数(eg: my_func)的元信息(eg: __name__)到装饰器内函数的拷贝。使用方法即在内函数前加上新的装饰器@wraps(inner)。可参考如下示例:

from functools import wraps

def my_logging(func):
    
    @wraps(func)
    def wrapper():

        print("{} begins.".format(func.__name__))
        return func()

    return wrapper


@my_logging     
def my_func():

    print("Welcome to my blog.")


if __name__ == "__main__":

    my_func()
    print(my_func.__name__)     # my_func

同样地,存储状态的装饰器可以大幅缩减程序的运行时间:

class MyCahe(object):
    
    def __init__(self, func):
        
        self.func = func
        self.cache = {}
    

    def __call__(self, *args):
        
        if not args in self.cache:
            self.cache[args] = self.func(*args)
        return self.cache[args]

@MyCahe
def fib(n):
    
    if n <= 1:
        return 1
    return fib(n - 1) + fib(n - 2)

print(fib(3))

需要注意的是,一个函数可以同时定义多个装饰器,执行顺序是由近到远:

@a
@b
@c
def f ():
    pass

等同于f=a(b(c(f)))


  1. 援引自刘志军的博客https://zhuanlan.zhihu.com/p/27449649。 ↩︎

  2. 关于闭包的解释援引自大江狗的博客 https://zhuanlan.zhihu.com/p/51158386,稍作修改。 ↩︎

Bad decisions make good stories.