一、装饰器概述
在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 的闭包特性
在从函数的嵌套定义到 Python 闭包中,还有如下结论:
- 指向函数对象的变量(即函数名)可被当作返回值返回;
- 内层嵌套函数可以访问并记录外层函数中定义的变量。
4. 必备四:Python 可调用对象本质
在 Python 中,函数和方法都被称作可调用对象。实际上,Python 中任何实现了魔法方法 __call__
的对象都是可调用对象。因此,Python 中,装饰器就是一个可调用对象,该可调用对象能够返回一个可调用对象。
实际上,可以通过 函数名_.__dir__
查看函数对象的确有魔法方法 __call__
。
三、装饰器初步探究
在 Python 中,装饰器可以是一个函数也可以是一个类,被装饰的对象也既可以是一个函数或者一个类。其中最简单的一种模式为:装饰器和被装饰对象都是一个函数,本文将主要围绕这类装饰器进行介绍,关于其他更高级的装饰器,如:装饰器和/或被装饰对象为类,请见Python装饰器进阶与高级应用。
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)
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分别被进一步传递。
3. 装饰有参数有返回值函数
进一步地,如果上述被装饰函数divide()有返回值,则对其进行装饰的代码如下:
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()的返回值。
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分别表示对元组和字典先进行拆包,然后再传递。
四、装饰器的部分简单应用
上面是对Python中装饰器进行的初步探究,下面是装饰器在实际程序中的部分简单应用。
首先,基于对上述讨论的总结,下面先给出用于介绍装饰器实际应用时用到的装饰器模板,后续更加复杂的装饰器也将基于该模板来修改。
import functools
def decorator(func):
@functools.wraps(func)
def wrapper_decorator(*args, **kwargs):
# Do something before
value = func(*args, **kwargs)
# Do something after
return value
return wrapper_decorator
关于程序中为何使用functools模块内的wrap()函数,请见:Python模块functools学习笔记。
1. 程序计时
下面的@timer装饰器可以测量一个函数执行所耗费的时间,并将该时间打印至控制台。
import functools
import time
def timer(func):
"""Print the runtime of the decorated function"""
@functools.wraps(func)
def wrapper_timer(*args, **kwargs):
start_time = time.perf_counter()
value = func(*args, **kwargs)
end_time = time.perf_counter()
run_time = end_time - start_time
print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
return value
return wrapper_timer
@timer
def waste_some_time(num_times):
for _ in range(num_times):
sum([i ** 2 for i in range(10000)])
waste_some_time(10)
需要注意的是:如果你只是希望了解自己的程序运行大概会耗费多长时间,那么自定的@timer装饰器已经足够。如果你希望对程序运行时间做更精确的度量,你需要考虑使用Python标准库中的timeit模块。该模块会暂时性关闭Python解释器的垃圾回收以多次运行待测量代码以剔除噪声。
2. 调试代码
下面的@debug装饰器会在被装饰函数每次执行时,打印函数的参数以及返回值:
import functools
def debug(func):
"""Print the function signature and return value"""
@functools.wraps(func)
def wrapper_debug(*args, **kwargs):
args_repr = [repr(a) for a in args] # 1
kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()] # 2
signature = ", ".join(args_repr + kwargs_repr) # 3
print(f"Calling {func.__name__}({signature})")
value = func(*args, **kwargs)
print(f"{func.__name__!r} returned {value!r}") # 4
return value
return wrapper_debug
@debug
def make_greeting(name, age=None):
if age is None:
return f"Howdy {name}!"
else:
return f"Whoa {name}! {age} already, you are growing up!"
print(make_greeting("Benjamin"), end="\n" * 2)
print(make_greeting("Richard", age=112), end="\n" * 2)
print(make_greeting(name="Dorrisile", age=116), end="\n" * 2)
上述代码的执行结果为:
Calling make_greeting(‘Benjamin’)
‘make_greeting’ returned ‘Howdy Benjamin!’
Howdy Benjamin!
Calling make_greeting(‘Richard’, age=112)
‘make_greeting’ returned ‘Whoa Richard! 112 already, you are growing up!’
Whoa Richard! 112 already, you are growing up!
Calling make_greeting(name=‘Dorrisile’, age=116)
‘make_greeting’ returned ‘Whoa Dorrisile! 116 already, you are growing up!’
Whoa Dorrisile! 116 already, you are growing up!
下面简单分析上述实现@debug装饰器代码原理和涉及的Python语法点,上述代码中的注释编号对应于如下有序列表的编号:
- 创建一个由位置参数组成的列表。使用repr()函数以一种无歧义的方式获得代表每个参数的字符串表现形式。
- 创建一个由关键字参数组成的列表。其中
f-string
用来以key=value
的形式格式化字符串。 - 位置参数和关键字参数由逗号拼接成一个签名。
- 返回值在函数被执行后得到打印。
上述@debug
装饰器还能用于Python中标准库函数,如下列代码用于计算数学中的常量
e
e
e的值,其计算公式为:
e = ∑ n = 0 ∞ 1 n ! = 1 0 ! + 1 1 ! + 1 2 ! + ⋅ ⋅ ⋅ = 1 1 + 1 1 + 1 1 × 2 + ⋅ ⋅ ⋅ e={\sum_{n=0}^\infty\frac{1}{n!}}=\frac{1}{0!}+\frac{1}{1!}+\frac{1}{2!}+\cdot\cdot\cdot=\frac{1}{1}+\frac{1}{1}+\frac{1}{1\times2}+\cdot\cdot\cdot e=n=0∑∞n!1=0!1+1!1+2!1+⋅⋅⋅=11+11+1×21+⋅⋅⋅
import math
from decorators import debug
# 应用自定义装饰器于Python标准库函数
math.factorial = debug(math.factorial)
def approximate_e(terms=18):
return sum(1 / math.factorial(each) for each in range(terms))
def main():
print(approximate_e(terms=5))
if __name__ == '__main__':
main()
上述代码的运行结果为:
Calling factorial(0)
‘factorial’ returned 1
Calling factorial(1)
‘factorial’ returned 1
Calling factorial(2)
‘factorial’ returned 2
Calling factorial(3)
‘factorial’ returned 6
Calling factorial(4)
‘factorial’ returned 24
2.708333333333333
需要说明的是,为了代码的解耦合与重用性,上面代码将装饰器函数都放在了文件decorators.py
中,在其他.py
文件中使用哪一个装饰器就导入对应的函数。
3. 代码减速
乍一看起来很奇怪,因为一般都希望代码的运行速度可以越快越好,但在一种情况下可能的确是必要的,如:一段代码需要连续检查网页资源是否已经更新。下面的例子可以实现调用代码前延时一秒钟:
import functools
import time
def slow_down(func):
"""调用函数之前延时一秒钟"""
@functools.wraps(func)
def wrapper_slow_down(*args, **kwargs):
time.sleep(1)
return func(*args, **kwargs)
return wrapper_slow_down
@slow_down
def countdown(from_number):
if from_number < 1:
print("发射!")
else:
print(from_number)
countdown(from_number - 1)
def main():
countdown(from_number=5)
if __name__ == '__main__':
main()
上述代码的运行结果为:
5
4
3
2
1
发射!
4. 注册插件
事实上,装饰器也一定要改变被装饰函数的行为,装饰器也可以仅仅注册一下一个函数,这一特点可以用于创建一个轻量级的插件架构,如下列示例代码:
import random
import functools
PLUGINS = dict()
def register(func):
"""以插件形式注册一个函数"""
PLUGINS[func.__name__] = func
@functools.wraps(func) # 1
def wrap(name): # 2
return func(name) # 3
return wrap # 4
@register
def say_hello(name):
return f"Hello {name}"
@register
def be_awesome(name):
return f"Yo {name}, together we are the awesomest!"
def randomly_greet(name):
greeter, greeter_func = random.choice(list(PLUGINS.items()))
print(f"Using {greeter!r}")
return greeter_func(name)
def main():
print(PLUGINS)
print(randomly_greet("Eric Idle"))
if __name__ == '__main__':
main()
上述代码的运行结果为:
{‘say_hello’: <function say_hello at 0x7fc47a282ea0>, ‘be_awesome’: <function be_awesome at 0x7fc47a282f28>}
Using ‘say_hello’
Hello Eric Idle
上述@register
装饰器仅在全局字典变量PLUGINS
中存储被装饰函数的引用。需要注意的是:代码中位置#1、#2、#3、#4
处的代码均可省略,外部函数作用域内且内层函数作用域外之间的代码会在执行@register
就被执行。