在这篇文章中,我们将看看装饰器是什么,以及如何创建和使用它们。

装饰器为调用高阶函数(higher-order functions)提供了一种简单的语法。根据定义,装饰器是一个函数,它接受另一个函数并扩展这个函数的行为而不显式修改它。

函数

在了解装饰器之前,必须首先了解函数的工作原理。一个函数返回基于给定参数的值。这是一个非常简单的例子:

1
2
3
4
5
>>> def add_one(number):
... return number + 1

>>> add_one(2)
3

通常,Python中的函数有副作用,不仅仅是将输入转换为输出。 print()函数就是一个的例子:它返回None,同时产生一个副作用,向控制台输出内容。

在函数式编程(functional programming)中,(几乎)只使用纯函数而没有副作用。虽然Python不是纯函数式语言,但它支持许多函数式编程概念,包括作为第一类对象(First-Class Objects)的函数。

如果要理解装饰器,将函数视为一个将参数转换为值的东西就足够了。

第一类对象

在Python中,函数是第一类对象(first-class objects)。这意味着函数可以传递并当作参数,就像任何其他对象(string,int,float,list等)一样。考虑以下三个函数:

1
2
3
4
5
6
7
8
def say_hello(name):
return f"Hello {name}"

def be_awesome(name):
return f"Yo {name}, together we are the awesomest!"

def greet_bob(greeter_func):
return greeter_func("Bob")

这里,say_hello()be_awesome()是常规函数,它们返回一个字符串。 但是,greet_bob()函数需要一个函数作为参数。例如,我们可以将say_hello()be_awesome()函数作为参数传递给它:

1
2
3
4
5
>>> greet_bob(say_hello)
'Hello Bob'

>>> greet_bob(be_awesome)
'Yo Bob, together we are the awesomest!'

注意greet_bob(say_hello) 引用了两个函数,但使用的方式不同:greet_bob()say_hellosay_hello函数的名称没有括号。这意味着只传递对函数的引用,并未执行say_hello。另一方面,greet_bob()函数有括号,因此它将像往常一样调用。

内部函数

可以在其他函数中定义函数。 这些函数称为内部函数( inner functions)。 这是一个具有两个内部函数的函数示例:

1
2
3
4
5
6
7
8
9
10
11
def parent():
print("Printing from the parent() function")

def first_child():
print("Printing from the first_child() function")

def second_child():
print("Printing from the second_child() function")

second_child()
first_child()

当我们调用parent()函数时:

1
2
3
4
>>> parent()
Printing from the parent() function
Printing from the second_child() function
Printing from the first_child() function

定义内部函数的顺序无关紧要,与任何其他函数一样,打印仅在执行内部函数时才会发生。

此外,在调用父函数之前,内部函数不会被定义。它们的局部作用域在parent()中:它们只作为局部变量存在于parent()函数中。试着调用first_child(),会得到一个错误:

1
2
3
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'first_child' is not defined

从函数中返回函数

Python还允许使用函数作为返回值。下面的示例从外部的parent()函数返回一个内部函数:

1
2
3
4
5
6
7
8
9
10
11
def parent(num):
def first_child():
return "Hi, I am Emma"

def second_child():
return "Call me Liam"

if num == 1:
return first_child
else:
return second_child

返回的是没有括号的first_child。回想一下,这意味着返回对函数first_child的引用。相反,带有括号的first_child()表示执行函数的结果。这可以在下面的例子中看到:

1
2
3
4
5
6
7
8
>>> first = parent(1)
>>> second = parent(2)

>>> first
<function parent.<locals>.first_child at 0x7f599f1e2e18>

>>> second
<function parent.<locals>.second_child at 0x7f599dad5268>

我们可以看到,first变量引用了parent()中的内部的first_child()函数,而second则指向second_child()

现在可以使用firstsecond,它们就像是常规函数一样,即使它们指向的函数无法直接访问:

1
2
3
4
5
>>> first()
'Hi, I am Emma'

>>> second()
'Call me Liam'

最后,在之前的示例中,父函数中执行了内部函数,例如first_child()。但是,在最后一个示例中,在返回时没有向内部函数first_child添加括号。这样,就得到了将来可以调用的每个函数的引用。

简单的装饰器

现在已经了解了函数与Python中的任何其他对象一样,可以继续学习Python装饰器了。让我们从一个简单的例子开始:

1
2
3
4
5
6
7
8
9
10
11
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper

def say_whee():
print("Whee!")

say_whee = my_decorator(say_whee)

猜测一下调用say_whee()时会发生什么吗?试一试:

1
2
3
4
>>> say_whee()
Something is happening before the function is called.
Whee!
Something is happening after the function is called.

如果不理解这里发生了什么,请回顾上一节,这里实际上是在应用之前学到的所有东西。

所谓的装饰发生在这一行:

1
say_whee = my_decorator(say_whee)

实际上,say_whee现在指向内部函数wrapper()。当调用my_decorator(say_whee)时,将wrapper作为函数返回:

1
2
>>> say_whee
<function my_decorator.<locals>.wrapper at 0x7f3c5dfd42f0>

但是,wrapper()引用原来的say_whee()作为func,并在两个调用之间调用该函数来print()

简单地说:decorator封装了一个函数,并扩展了它的行为

在继续之前,让我们来看第二个例子。由于wrapper()是一个常规的Python函数,装饰器修改函数的方式可以动态地改变。下面的示例将只在白天运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from datetime import datetime

def not_during_the_night(func):
def wrapper():
if 7 <= datetime.now().hour < 22:
func()
else:
pass
return wrapper

def say_whee():
print("Whee!")

say_whee = not_during_the_night(say_whee)

如果在睡觉前尝试调用say_whee(),则不会发生任何事情:

1
2
>>> say_whee()
>>>

语法糖

这样装饰say_whee()的方式有点笨重。 首先,需要输入say_whee这个名称三次。此外,装饰可能会被隐藏在函数的定义之下。

因此,Python允许以一种更简单的方式使用@符号(有时称为“pie”语法)的装饰器。下面的示例与第一个装饰器示例完全相同:

1
2
3
4
5
6
7
8
9
10
def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper

@my_decorator
def say_whee():
print("Whee!")

因此,@my_decorator是一种更简单的方法来表示say_whee = my_decorator(say_whee),这就是如何将装饰器应用到函数上。

重用装饰器

回想一下,装饰器只是一个常规的Python函数。所有常用的工具都可以轻松重复使用。让我们将装饰器移动到可以在许多其他功能中使用的模块中。

使用以下内容创建名为decorators.py的文件:

1
2
3
4
5
def do_twice(func):
def wrapper_do_twice():
func()
func()
return wrapper_do_twice

可以根据需要命名内部函数,并且通常可以使用wrapper()这样的通用名称。在本文中有很多装饰器。为了使它们分开,我们将内部函数命名为与装饰器同名但带有wrapper_前缀。

现在可以通过常规导入在其他文件中使用这个新装饰器:

1
2
3
4
5
from decorators import do_twice

@do_twice
def say_whee():
print("Whee!")

运行此示例时,原来的say_whee()会执行两次:

1
2
3
>>> say_whee()
Whee!
Whee!

用参数装饰函数

假设有一个接受一些参数的函数,还能装饰这个函数吗?

1
2
3
4
5
from decorators import do_twice

@do_twice
def greet(name):
print(f"Hello {name}")

实际上会报错:

1
2
3
4
>>> greet("World")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: wrapper_do_twice() takes 0 positional arguments but 1 was given

问题在于内部函数wrapper_do_twice()不接受任何参数,但是传递了name ="World"。可以通过让wrapper_do_twice()接受一个参数来解决这个问题,但这样的话它对之前创建的函数say_whee()将不起作用。

解决的方法是在内部的wrapper函数中使用 *args and **kwargs。 它将接受任意数量的位置参数和关键字参数。重写decorators.py如下:

1
2
3
4
5
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
func(*args, **kwargs)
return wrapper_do_twice

内部函数wrapper_do_twice()现在接受任意数量的参数并将它们传递给它所装饰的函数。现在say_whee()greet()示例都有效:

1
2
3
4
5
6
7
>>> say_whee()
Whee!
Whee!

>>> greet("World")
Hello World
Hello World

从装饰函数返回值

被装饰函数的返回值会发生什么变化? 这由装饰器来决定:

1
2
3
4
5
6
from decorators import do_twice

@do_twice
def return_greeting(name):
print("Creating greeting")
return f"Hi {name}"

运行一下:

1
2
3
4
5
>>> hi_adam = return_greeting("Adam")
Creating greeting
Creating greeting
>>> print(hi_adam)
None

Well,装饰器吃了函数的返回值。

因为do_twice_wrapper()没有显式的返回值,所以调用return_greeting("Adam")最终返回None

要解决此问题,需要确保wrapper函数返回被装饰函数的返回值。更改decorators.py文件:

1
2
3
4
5
def do_twice(func):
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice

这样返回了上次执行函数的返回值:

1
2
3
4
>>> return_greeting("Adam")
Creating greeting
Creating greeting
'Hi Adam'

函数的身份

使用Python非常方便,特别是在交互式shell中,它具有强大的内省功能(Introspection)。内省是对象在运行时了解其自身属性的能力。例如,函数知道自己的名称和文档:

1
2
3
4
5
6
7
8
9
10
11
>>> print
<built-in function print>

>>> print.__name__
'print'

>>> help(print)
Help on built-in function print in module builtins:

print(...)
<full help message>

内省也适用于我们自己定义的函数:

1
2
3
4
5
6
7
8
9
10
>>> say_whee
<function do_twice.<locals>.wrapper_do_twice at 0x7f43700e52f0>

>>> say_whee.__name__
'wrapper_do_twice'

>>> help(say_whee)
Help on function wrapper_do_twice in module decorators:

wrapper_do_twice()

然而,经过装饰之后,say_whee()对其身份感到非常困惑。它现在报告成为do_twice()装饰器内的内部函数wrapper_do_twice()。虽然在技术上是正确的,但这并没有用。

要修复这个问题,装饰器应该使用@functools.wrapper装饰器,它将保留关于原始函数的信息。更新decorators.py

1
2
3
4
5
6
7
8
import functools

def do_twice(func):
@functools.wraps(func)
def wrapper_do_twice(*args, **kwargs):
func(*args, **kwargs)
return func(*args, **kwargs)
return wrapper_do_twice

同时不需要对已装饰的say_whee()函数进行任何更改:

1
2
3
4
5
6
7
8
9
10
>>> say_whee
<function say_whee at 0x7ff79a60f2f0>

>>> say_whee.__name__
'say_whee'

>>> help(say_whee)
Help on function say_whee in module whee:

say_whee()

@functools.wrapper 装饰器使用函数functions.update_wrapper()来更新内省中使用的__name____doc__等特殊属性。

更加有用的例子

让我们来看几个更有用的装饰器示例。

1
2
3
4
5
6
7
8
9
10
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

这个公式是一个很好的样板模板,用于构建更复杂的装饰器。

在后面的示例中,我们假设这些装饰器也保存在decorators.py文件中。

计时函数

让我们从创建 @timer 装饰器开始。它将测量函数执行所需的时间,并将函数持续时间打印到控制台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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() # 1
value = func(*args, **kwargs)
end_time = time.perf_counter() # 2
run_time = end_time - start_time # 3
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)])

装饰器记录函数开始运行前的时间(# 1)和函数完成后的时间(# 2),函数运行所花费的时间为两者之差(# 3)。我们使用time.perf_counter()函数,它很好地测量时间间隔。以下是一些计时的例子:

1
2
3
4
5
>>> waste_some_time(1)
Finished 'waste_some_time' in 0.0010 secs

>>> waste_some_time(999)
Finished 'waste_some_time' in 0.3260 secs

注意:如果只是想了解函数的运行时间,那么@timer装饰器就很棒。 如果要对代码进行更精确的测量,则应考虑标准库中的timeit模块。它暂时禁用垃圾回收并运行多次试验以消除快速调用函数中影响运行时间的噪声。

调试代码

以下 @debug 装饰器将在每次调用函数时打印调用函数的参数及其返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

签名是通过连接所有参数的字符串表示( string representations )形式创建的。以下列表中的数字对应于代码中的编号注释:

  1. 创建位置参数列表。使用repr()获取表示每个参数的字符串。
  2. 创建关键字参数列表。 f-string 将每个参数格式化为key = value,其中!r说明符表示repr()用于表示值。
  3. 位置参数和关键字参数的列表连接到一个签名字符串,每个参数用逗号分隔。
  4. 返回值在函数执行后打印。

让我们看看装饰器是如何在实践中工作的,把它应用到一个简单的函数,只有一个位置参数和一个关键字参数:

1
2
3
4
5
6
@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!"

注意@debug装饰器如何打印make_greeting()函数的签名和返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> make_greeting("Benjamin")
Calling make_greeting('Benjamin')
'make_greeting' returned 'Howdy Benjamin!'
'Howdy Benjamin!'

>>> make_greeting("Richard", age=112)
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!'

>>> make_greeting(name="Dorrisile", age=116)
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装饰器只是重复了刚才写的内容。当应用到那些不直接调用的方便的小的函数时,它会更加强大。

下面的例子计算了一个近似的数学常数e:

1
2
3
4
5
6
7
8
import math
from decorators import debug

# Apply a decorator to a standard library function
math.factorial = debug(math.factorial)

def approximate_e(terms=18):
return sum(1 / math.factorial(n) for n in range(terms))

这个示例还展示了如何将装饰器应用于已经定义的函数。e的近似基于以下级数展开:

e=n=01n!e=\sum^{\infty}_{n=0}{\frac{1}{n!}}

在调用approximate_e()函数时,可以看到@debug装饰器很有用:

1
2
3
4
5
6
7
8
9
10
11
12
>>> approximate_e(5)
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

减慢代码速度

下一个示例可能看起来不太有用。为什么要放慢Python代码? 可能最常见的用例是希望对一个资源(如网页)持续查看是否已更改的函数,进行速率限制。 @slow_down装饰器会在调用被装饰函数之前休眠一秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import functools
import time

def slow_down(func):
"""Sleep 1 second before calling the function"""
@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("Liftoff!")
else:
print(from_number)
countdown(from_number - 1)

运行该示例,查看@slow_down装饰器的效果:

1
2
3
4
5
>>> countdown(3)
3
2
1
Liftoff!

@slow_down装饰器sleep一秒钟。之后,将看到如何通过向装饰器传递参数来控制速率。

注册插件

装饰器可以不必包装他们正在装饰的函数,他们也可以简单地注册一个函数并将其解包返回。例如,可用于创建轻量级插件架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import random
PLUGINS = dict()

def register(func):
"""Register a function as a plug-in"""
PLUGINS[func.__name__] = func
return func

@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)

@register装饰器在全局PLUGINS字典中存储被装饰函数的引用。在本例中使用包装,因为返回的是未修改的原始函数。

randomly_greet()函数随机选择要使用的注册函数。注意,插件字典已经包含了对注册为插件的每个函数对象的引用:

1
2
3
4
5
6
7
>>> PLUGINS
{'say_hello': <function say_hello at 0x7f768eae6730>,
'be_awesome': <function be_awesome at 0x7f768eae67b8>}

>>> randomly_greet("Alice")
Using 'say_hello'
'Hello Alice'

这个简单的插件架构的主要好处是不需要维护一个已有插件的列表。这个列表是在插件注册时创建的。这使得添加一个新插件变得很简单:只需定义函数并用@register装饰即可。

如果熟悉Python中的globals(),可能会看到一些插件架构的相似之处。globals()允许访问当前范围内的所有全局变量,包括插件:

1
2
3
4
5
>>> globals()
{..., # Lots of variables not shown here.
'say_hello': <function say_hello at 0x7f768eae6730>,
'be_awesome': <function be_awesome at 0x7f768eae67b8>,
'randomly_greet': <function randomly_greet at 0x7f768eae6840>}

使用@register装饰器,可以创建自己的感兴趣变量的管理列表,更为有效地从globals()中挑选函数。

登录检查

在继续讨论一些更有趣的装饰器之前,我们看看这一节的最后一个示例。

这个实例通常在处理web框架时使用,在本例中,我们使用Flask来设置一个/secret web页面,该页面只对登录或以其他方式进行身份验证的用户可见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from flask import Flask, g, request, redirect, url_for
import functools
app = Flask(__name__)

def login_required(func):
"""Make sure user is logged in before proceeding"""
@functools.wraps(func)
def wrapper_login_required(*args, **kwargs):
if g.user is None:
return redirect(url_for("login", next=request.url))
return func(*args, **kwargs)
return wrapper_login_required

@app.route("/secret")
@login_required
def secret():
...

虽然这提供了如何向web框架添加身份验证的概念,但通常一般不自己编写这些类型的装饰器。对于Flask,您可以使用 the Flask-Login extension,这增加了更多的安全性和功能性。