经过入门Python装饰器深入Python装饰器,我们已经知道如何创建各种各样的装饰器,现在让我们看一些在现实世界更有用的Python装饰器示例,来帮助我们更好的理解与使用Python装饰器

重新回顾示例——减慢代码速度

正如入门Python装饰器提到的,我们以前的@slow_down实现总是休眠一秒钟。现在已经知道了如何向装饰器添加参数,因此让我们使用一个控制其休眠时间的可选rate参数重写@slow_down:

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

def slow_down(_func=None, *, rate=1):
"""Sleep given amount of seconds before calling the function"""
def decorator_slow_down(func):
@functools.wraps(func)
def wrapper_slow_down(*args, **kwargs):
time.sleep(rate)
return func(*args, **kwargs)
return wrapper_slow_down

if _func is None:
return decorator_slow_down
else:
return decorator_slow_down(_func)

我们使用深入Python装饰器中介绍的样板文件,让@slow_down可以带可选的参数。递归countdown()函数现在在每次计数之间休眠2秒:

1
2
3
4
5
6
7
@slow_down(rate=2)
def countdown(from_number):
if from_number < 1:
print("Liftoff!")
else:
print(from_number)
countdown(from_number - 1)

查看一下效果

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

创建单例(Singletons)

单例是一个只有一个实例的类。Python中有几种常用的单例,包括NoneTrueFalse。事实上,None是一个单例,允许你使用is关键字比较None

1
2
3
4
if _func is None:
return decorator_name
else:
return decorator_name(_func)

使用is只对完全相同实例的对象返回True。下面的@singleton装饰器将类的第一个实例存储为属性,从而将类转换为单例对象。稍后创建实例的尝试只是返回存储的实例:

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

def singleton(cls):
"""Make a class a Singleton class (only one instance)"""
@functools.wraps(cls)
def wrapper_singleton(*args, **kwargs):
if not wrapper_singleton.instance:
wrapper_singleton.instance = cls(*args, **kwargs)
return wrapper_singleton.instance
wrapper_singleton.instance = None
return wrapper_singleton

@singleton
class TheOne:
pass

如上所示,这个类装饰器遵循与我们的函数装饰器相同的模板。唯一的区别是,我们使用cls作为参数名来表示它是一个类装饰器,而不是func

让我们看一下代码运行结果:

1
2
3
4
5
6
7
8
9
10
11
>>> first_one = TheOne()
>>> another_one = TheOne()

>>> id(first_one)
140094218762280

>>> id(another_one)
140094218762280

>>> first_one is another_one
True

很明显,first_one确实与another_one完全相同。

在Python中,单例类的使用并不像在其他语言中那样频繁。单例的效果通常在模块中作为全局变量更好地实现。

缓存返回值

装饰器可以提供一种很好的缓存和记忆机制。让我们来看看斐波那契数列的递归定义:

1
2
3
4
5
6
7
from decorators import count_calls

@count_calls
def fibonacci(num):
if num < 2:
return num
return fibonacci(num - 1) + fibonacci(num - 2)

虽然实现很简单,但运行时性能却很糟糕:

1
2
3
4
5
6
>>> fibonacci(10)
<Lots of output from count_calls>
55

>>> fibonacci.num_calls
177

要计算第10个斐波那契数,实际上只需要计算前面的斐波那契数,但是这个实现需要大量的177次计算。更糟糕的是:fibonacci(20)需要21891次计算,第30次需要270万次计算。这是因为代码一直在重新计算已知的斐波那契数。

通常的解决方案是使用for循环和查找表来实现斐波那契数。然而,简单的计算缓存也可以做到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import functools
from decorators import count_calls

def cache(func):
"""Keep a cache of previous function calls"""
@functools.wraps(func)
def wrapper_cache(*args, **kwargs):
cache_key = args + tuple(kwargs.items())
if cache_key not in wrapper_cache.cache:
wrapper_cache.cache[cache_key] = func(*args, **kwargs)
return wrapper_cache.cache[cache_key]
wrapper_cache.cache = dict()
return wrapper_cache

@cache
@count_calls
def fibonacci(num):
if num < 2:
return num
return fibonacci(num - 1) + fibonacci(num - 2)

缓存作为查找表工作,所以现在fibonacci()只执行一次必要的计算:

1
2
3
4
5
6
7
8
>>> fibonacci(10)
Call 1 of 'fibonacci'
...
Call 11 of 'fibonacci'
55

>>> fibonacci(8)
21

注意,在对fibonacci(8)的最后调用中,不需要进行新的计算,因为已经为fibonacci(10)计算了第8个斐波那契数。

在标准库中, Least Recently Used (LRU) cache 提供于 @functools.lru_cache

这个装饰器的特性比上面看到的要多很多。应该使用@functools.lru_cache,而不是编写自己的缓存装饰器:

1
2
3
4
5
6
7
8
import functools

@functools.lru_cache(maxsize=4)
def fibonacci(num):
print(f"Calculating fibonacci({num})")
if num < 2:
return num
return fibonacci(num - 1) + fibonacci(num - 2)

maxsize参数指定最近缓存了多少调用。默认值是128,但是您可以指定maxsize=None来缓存所有函数调用。但是,请注意,如果正在缓存许多大型对象,这可能会导致内存问题。

也可以使用.cache_info()方法查看缓存的执行情况,并在需要时对其进行调优。在我们的示例中,我们使用一个较小的maxsize来查看从缓存中删除元素的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
>>> fibonacci(10)
Calculating fibonacci(10)
Calculating fibonacci(9)
Calculating fibonacci(8)
Calculating fibonacci(7)
Calculating fibonacci(6)
Calculating fibonacci(5)
Calculating fibonacci(4)
Calculating fibonacci(3)
Calculating fibonacci(2)
Calculating fibonacci(1)
Calculating fibonacci(0)
55

>>> fibonacci(8)
21

>>> fibonacci(5)
Calculating fibonacci(5)
Calculating fibonacci(4)
Calculating fibonacci(3)
Calculating fibonacci(2)
Calculating fibonacci(1)
Calculating fibonacci(0)
5

>>> fibonacci(8)
Calculating fibonacci(8)
Calculating fibonacci(7)
Calculating fibonacci(6)
21

>>> fibonacci(5)
5

>>> fibonacci.cache_info()
CacheInfo(hits=17, misses=20, maxsize=4, currsize=4)

添加关于单位的信息

以下示例有点类似于入门Python装饰器中的Registering Plugins示例,因为它并未真正更改已装饰函数的行为。相反,它只是将unit添加为函数属性:

1
2
3
4
5
6
def set_unit(unit):
"""Register a unit on a function"""
def decorator_set_unit(func):
func.unit = unit
return func
return decorator_set_unit

下面的示例根据圆柱体的半径和高度(以厘米为单位)计算其体积:

1
2
3
4
5
import math

@set_unit("cm^3")
def volume(radius, height):
return math.pi * radius**2 * height

这个.unit函数属性稍后可以在需要时访问:

1
2
3
4
5
>>> volume(3, 5)
141.3716694115407

>>> volume.unit
'cm^3'

也可以使用函数注释( function annotations)实现类似的功能:

1
2
3
4
import math

def volume(radius, height) -> "cm^3":
return math.pi * radius**2 * height

但是,由于注释用于类型提示,因此很难将单位信息与有静态类型检查的注释相结合。

当连接到一个可以在单位之间转换的库,单位变得更加强大和有趣。一个这样的库是pint。安装pint(pip install Pint),可以转换体积为立方英寸或加仑:

1
2
3
4
5
6
7
8
9
10
11
12
>>> import pint
>>> ureg = pint.UnitRegistry()
>>> vol = volume(3, 5) * ureg(volume.unit)

>>> vol
<Quantity(141.3716694115407, 'centimeter ** 3')>

>>> vol.to("cubic inches")
<Quantity(8.627028576414954, 'inch ** 3')>

>>> vol.to("gallons").m # Magnitude
0.0373464440537444

还可以修改装饰器,直接返回pint Quantity。这样的Quantity是通过与单位相乘得到的。在pint中,单位必须在UnitRegistry中查找。注册表作为函数属性存储,以避免混淆名称空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def use_unit(unit):
"""Have a function return a Quantity with given unit"""
use_unit.ureg = pint.UnitRegistry()
def decorator_use_unit(func):
@functools.wraps(func)
def wrapper_use_unit(*args, **kwargs):
value = func(*args, **kwargs)
return value * use_unit.ureg(unit)
return wrapper_use_unit
return decorator_use_unit

@use_unit("meters per second")
def average_speed(distance, duration):
return distance / duration

有了@use_unit装饰器,转换单位毫不费力:

1
2
3
4
5
6
7
8
9
>>> bolt = average_speed(100, 9.58)
>>> bolt
<Quantity(10.438413361169102, 'meter / second')>

>>> bolt.to("km per hour")
<Quantity(37.578288100208766, 'kilometer / hour')>

>>> bolt.to("mph").m # Magnitude
23.350065679064745

验证JSON

让我们看最后一个用例。快速查看以下 Flask路由处理程序:

1
2
3
4
5
6
7
@app.route("/grade", methods=["POST"])
def update_grade():
json_data = request.get_json()
if "student_id" not in json_data:
abort(400)
# Update database
return "success!"

这里我们确保key student_id是请求的一部分。尽管这种验证有效,但它实际上并不属于函数本身。另外,可能还有其他使用相同验证的路由。因此,让我们保持DRY,并使用装饰器抽象出任何不必要的逻辑。下面的@validate_json装饰器将完成这项工作:

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

def validate_json(*expected_args): # 1
def decorator_validate_json(func):
@functools.wraps(func)
def wrapper_validate_json(*args, **kwargs):
json_object = request.get_json()
for expected_arg in expected_args: # 2
if expected_arg not in json_object:
abort(400)
return func(*args, **kwargs)
return wrapper_validate_json
return decorator_validate_json

在上面的代码中,装饰器采用了一个可变长度列表作为参数,这样我们就可以传递尽可能多的字符串参数,每个参数都代表一个用于验证JSON数据的key:

  1. JSON中必须出现的key列表作为参数提供给装饰器。
  2. 包装器函数验证JSON数据中出现的每个预期key

然后,路由处理程序可以关注其真正作业——更新级别——因为它可以安全地假设JSON数据是有效的:

1
2
3
4
5
6
@app.route("/grade", methods=["POST"])
@validate_json("student_id")
def update_grade():
json_data = request.get_json()
# Update database.
return "success!"

总结

这确是一段相当艰难的旅程!

入门Python装饰器通过仔细研究函数来开始,特别是如何在其他函数中定义装饰器,并像其他任何Python对象一样传递装饰器。

然后你学习了装饰器以及如何编写装饰器:

  • 装饰器可以重复使用
  • 装饰器可以用参数和返回值修饰函数
  • 装饰器可以使用@functools.wraps看起来更像装饰函数

深入Python装饰器中,我们看到了更高级的装饰器,并了解了如何:

  • 装饰类
  • 嵌套装饰器
  • 向装饰器添加参数
  • 在装饰器内部保寸状态
  • 使用类作为装饰器

已经看到,要定义装饰器,通常需要定义返回包装器函数的函数。
包装器函数使用*args**kwargs将参数传递给被装饰函数。如果希望装饰器也包含参数,则需要将包装器函数嵌套在另一个函数中。在这种情况下,通常会以三个return语句结束。

更进一步 Further Reading

可以参阅 Python Cookbook 一书

有关如何在Python中实现装饰器的历史讨论,请参阅 PEP 318 以及 Python Decorator Wiki。更多装饰器的例子可以在Python装饰器库中找到。 decorator module可以简化创建自己的装饰器的过程,其 documentation 中还包含了更进一步的装饰器示例。