一、装饰器概述

在Python中,装饰器是一种设计模式,装饰器可以在不改变被装饰对象(函数、类)的同时为对象增添新的功能。这也被成为元编程(metaprogramming),因为一部分代码尝试在编译时修改另一部分代码。

二、装饰器理解必备知识

1. 必备一:Python中一切皆对象

在Python中,一切皆对象,包括函数、类等,而变量名只是用于指向对象的标识符,多个不同的变量可以指向同一个对象。如下面代码表明两个变量名指向了同一个函数对象,则以两个变量名加上()可以调用同一个函数。


def plus_one(number):
    return number + 1


add_one = plus_one
print("add_one(5) = %d" % add_one(5))


代码运行结果为:


add_one(5) = 6


2. 必备二:函数可作为参数传递

在Python中,指向函数的变量可以作为参数传递给另外一个函数,如下述代码所述:


def plus_one(number):
    return number + 1


def function_call(function):
    number_to_add = 5
    return function(number_to_add)


print("function_call(plus_one) = %d" % function_call(plus_one))


代码运行结果为:


function_call(plus_one) = 6


3. 必备三:Python中的闭包特性

  • 指向函数对象的变量(即函数名)可被当作返回值返回;
  • 内层嵌套函数可以访问并记录外层函数中定义的变量。

4. 必备四:Python可调用对象本质

在Python中,函数和方法都被称作可调用对象。实际上,Python中任何实现了魔法方法__call__的对象都是可调用对象。因此,Python中,装饰器就是一个可调用对象,该可调用对象能够返回一个可调用对象。

实际上,可以通过函数名.__dir__()查看函数对象的确有魔法方法__call__。

三、装饰器探究

1. 函数装饰器

1.1 装饰普通函数

基于上述必备知识,先看下列代码示例:


def make_pretty(func):
    def inner():
        print("I got decorated")
        func()

    return inner


def ordinary():
    print("I am ordinary")


def main():
    ordinary()

    pretty = make_pretty(ordinary)
    pretty()


if __name__ == '__main__':
    main()


上述代码的运行结果为:


I am ordinary
I got decorated
I am ordinary


上述示例代码中,make_pretty()就是一个装饰器,在下述步骤中:


pretty = make_pretty(ordinary)


函数ordinary()被装饰,且装饰器的返回值被赋给了变量pretty。因此,装饰器函数在原函数的基础上增添了新的功能。

事实上,基于必备一,通常在装饰一个函数后,用以接收装饰器返回值的变量名和被装饰函数名保持一致,用上述例子,即:


ordinary = make_pretty(ordinary)


因此,Python中对此有如下简化语法(在Python,这就叫所谓的语法糖):


@make_pretty
def ordinary():
    print("I am ordinary")


即上述语法等价于:


def ordinary():
    print("I am ordinary")
ordinary = make_pretty(ordinary)


1.2 装饰带参数函数

上述装饰器非常简单且且只能装饰不带任何参数的函数。如果希望装饰如下所示函数:


def divide(a, b):
    return a/b


由于上述函数接受两个参数,且当传递b等于零时会发生异常,下面通过非捕获异常的方式完善上述代码:


def smart_divide(func):
    print("Preparing to decorate the divide func")

    def inner(a, b):
        print("I am going to divide", a, "and", b)
        if b == 0:
            print("Whoops! cannot divide because divisor is zero...")
            return

        func(a, b)

    return inner


@smart_divide
def divide(a, b):
    print("a / b = ", a / b)


def main():
    print("-" * 25)
    divide(2, 5)
    divide(2, 0)


if __name__ == '__main__':
    main()


上述代码的运行结果为:


Preparing to decorate the divide func
-------------------------
I am going to divide 2 and 5
a / b = 0.4
I am going to divide 2 and 0
Whoops! cannot divide because divisor is zero…


即上述代码完成了对于接收两个参数的函数进行装饰。事实上,由于:


@smart_divide
def divide(a, b):
    print("a / b = ", a / b)


等价于:


def divide(a, b):
    print("a / b = ", a / b)
divide = smart_divide(divide)


即此时变量divide和smart_divide的返回值inner同时指向嵌套函数处,则在第22、23行时,参数a、b相当于分别被传递至嵌套函数inner处。进而,在第10行调用func指向的原函数时,参数a、b分别被进一步传递。

1.3 装饰有返回值函数


def smart_divide(func):
    print("Preparing to decorate the divide func")

    def inner(a, b):
        print("I am going to divide", a, "and", b)
        if b == 0:
            print("Whoops! cannot divide because divisor is zero...")
            return

        return func(a, b)

    return inner


@smart_divide
def divide(a, b):
    return a / b


def main():
    print("-" * 25)
    
    ret1 = divide(2, 5)
    print(ret1)

	ret2 = divide(2, 0)
    print(ret2)


if __name__ == '__main__':
    main()


实际上,由于程序第23、26行调用被装饰后的divide()函数相当于调用inner函数,在调用inner()时,由于需要使用func调用指向被装饰前的divide()函数并确保后者仍旧正确返回,则需要在inner()中返回func()的返回值。

1.4 通用装饰器

实际上,为了使得装饰器可以通用,需要考虑Python中函数接收不定长参数的特性,为了确保装饰器能够较为通用,即对接收不定长(元组、字典)参数的函数进行装饰,则通用装饰器有如下格式:


def universal_decorator(func):
    def inner(*args, **kwargs):
        print("Decorative operations for func")
        return func(*args, **kwargs)
    return inner


需要注意的是,嵌套函数参数位置的args、kwargs分别表示元组和字典,而在嵌套函数中调用被装饰前函数时,使用的*args、**kwargs分别表示对元组和字典先进行拆包,然后再传递。

1.5 多个装饰器装饰一个函数

在Python,多个装饰器可对同一个函数进行装饰,如下列代码所示:


def star(func):
    def inner(*args, **kwargs):
        print("*" * 30)
        func(*args, **kwargs)
        print("*" * 30)

    return inner


def percent(func):
    def inner(*args, **kwargs):
        print("%" * 30)
        func(*args, **kwargs)
        print("%" * 30)

    return inner


@star
@percent
def printer(msg):
    print(msg)


def main():
    printer("Hello")


if __name__ == '__main__':
    main()


上述代码的运行结果为:


******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************


实际上,产生上述运行结果的原因在于,上述代码中:


@star
@percent
def printer(msg):
    print(msg)


等价于:


def printer(msg):
    print(msg)
printer = star(percent(printer))


即:先由装饰器percent对printer()函数进行装饰,然后再由装饰器star对被装饰后的printer()函数进行装饰。

1.6 带参数装饰器

1.7 装饰类

2. 类装饰器

下列代码演示了如何通过一个名为Test的类来装饰函数get_str。


class Test(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("这里是装饰器添加的功能...")
        return self.func(*args, **kwargs)


@Test  # 相当于get_str = Test(get_str)
def get_str(*args, **kwargs):
    return "Strings to be decorated..."


print(get_str())


实际上,第10行代码@Test相当于:


get_str = Test(get_str)


即Test(get_str)相当于创建实例对象,且对象名为get_str,且此时参数get_str被传入类的初始化方法。

又由必备四,在使用()调用对象时会默认调用对象的__call__()方法,则该方法相当于函数装饰器中的嵌套函数

四、装饰器总结

在Python中,装饰器可以在不改变函数源代码、不继承父类并重写父类方法的同时,动态更改函数、方法、类的功能。使用装饰器可以确保你的代码DRY(Don’t Repeat Yourself)。装饰器有如下几种应用场景:

  • 在Flask、Django等框架中实现验证;
  • 打日志;
  • 测量程序执行时间;
  • 同步。

五、参考资料

  • [1] Python Decorators
  • [2] Decorators in Python
  • [3] python3_装饰器__装饰器原则 / 装饰函数和方法 / 装饰类 / 嵌套函数 / 高阶函数 / 可调用对象