经过入门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 functoolsimport timedef 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中有几种常用的单例,包括None
、True
和False
。事实上,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 functoolsdef 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_oneTrue
很明显,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_calls177
要计算第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 functoolsfrom decorators import count_callsdef 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 mathdef 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 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 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 ) 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, abortimport functoolsapp = Flask(__name__) def validate_json (*expected_args) : 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: if expected_arg not in json_object: abort(400 ) return func(*args, **kwargs) return wrapper_validate_json return decorator_validate_json
在上面的代码中,装饰器采用了一个可变长度列表作为参数,这样我们就可以传递尽可能多的字符串参数,每个参数都代表一个用于验证JSON数据的key
:
JSON中必须出现的key
列表作为参数提供给装饰器。
包装器函数验证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() return "success!"
总结
这确是一段相当艰难的旅程!
入门Python装饰器 通过仔细研究函数来开始,特别是如何在其他函数中定义装饰器,并像其他任何Python对象一样传递装饰器。
然后你学习了装饰器以及如何编写装饰器:
装饰器可以重复使用
装饰器可以用参数和返回值修饰函数
装饰器可以使用@functools.wraps
看起来更像装饰函数
在深入Python装饰器 中,我们看到了更高级的装饰器,并了解了如何:
装饰类
嵌套装饰器
向装饰器添加参数
在装饰器内部保寸状态
使用类作为装饰器
已经看到,要定义装饰器,通常需要定义返回包装器函数的函数。
包装器函数使用*args
和**kwargs
将参数传递给被装饰函数。如果希望装饰器也包含参数,则需要将包装器函数嵌套在另一个函数中。在这种情况下,通常会以三个return
语句结束。
更进一步 Further Reading
可以参阅 Python Cookbook 一书
有关如何在Python中实现装饰器的历史讨论,请参阅 PEP 318 以及 Python Decorator Wiki 。更多装饰器的例子可以在Python装饰器库中找到。 decorator
module 可以简化创建自己的装饰器的过程,其 documentation 中还包含了更进一步的装饰器示例。