一、装饰器概述
在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_装饰器__装饰器原则 / 装饰函数和方法 / 装饰类 / 嵌套函数 / 高阶函数 / 可调用对象