在Python装饰器语法进阶中,我们学习了Python装饰器的进阶语法,那么这些看上去花里胡哨的语法究竟有何用途,如何才能加深对这些语法的理解并应用于实际的代码中,这是本文即将要探讨的问题。
一、应用案例
1. 再议为代码减速
在Python装饰器入门与简单应用的代码减速案例中,装饰器@slow_down
总是让被装饰函数休眠一秒,而在Python装饰器语法进阶中我们知道了如何为装饰器添加参数,因此下面重写@slow_down
代码,使之可以接收一个可选参数rate
,该参数用以指定被装饰函数countdown()
的休眠时间长度:
import functools
import time
def slow_down(_func=None, rate=1):
"""使得程序在被调用之前休眠由rate指定的时间"""
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) # 1
@slow_down(rate=2)
def countdown(from_number):
if from_number < 1:
print("发射!")
else:
print(from_number)
countdown(from_number - 1)
def main():
countdown(5)
if __name__ == '__main__':
main()
对于上述代码,需要说明的是,当不为装饰器传参数,即使用@slow_down
直接装饰函数时,此语法等价于countdown = slow_down(countdown)
,则被装饰函数的引用会被传递至_func
处,此时_func
不为None
,则slow_down()
函数会在# 1
处返回,此时# 1
相当于return wrapper_slow_down
,即使用@slow_down
和使用@decorator_slow_down
实现的效果一样。
至于为什么不能直接在# 1
处写成return wrapper_slow_down
,原因在于:当使用@slow_down
执行装饰操作时,wrapper_slow_down()
函数因为在最内部还未被定义,故此时编译器无法找到该变量。
2. 创建单例的对象
所谓单例是指这样一个类,该类只有一个实例,即无论使用该类创建多少次实例对象,返回的都是同一个实例。
实际上,在Python中你经常使用几个单例,如:None
,True
以及False
,同时也因为None
是一个单例,所以你才可以使用is
关键字来比较None
和另外一个对象,而只有当待比较的两个对象是统一个类的实例时,is
关键字的返回值才为True
。
下面的@singleton
装饰器就将一个类变成了一个单例,其实现方式为:
- 将该类的第一个实例保存为内层函数对象
wrapper_singleton
的一个属性; - 后续创建实例时都返回上述属性中保存的实例的引用。
import functools
def singleton(cls):
@functools.wraps(cls)
def wrapper_singleton(*args, **kwargs):
if not wrapper_singleton.instance:
wrapper_singleton.instance = cls(*args, **kwargs) # 4
return wrapper_singleton.instance
wrapper_singleton.instance = None # 1
print("-" * 30) # 2
return wrapper_singleton # 3
@singleton # TheOne = singleton(TheOne) = wrapper_singleton
class TheOne(object):
pass
def main():
first_one = TheOne()
second_one = TheOne()
print(first_one is second_one)
print("id(first_one) = ", id(first_one))
print("id(second_one) = ", id(second_one))
if __name__ == '__main__':
main()
上述代码的运行结果为:
------------------------------
first_one is second_one = True
id(first_one) = 139627414325064
id(second_one) = 139627414325064
由上述运行结果可知,@singleton
装饰器的确将类TheOne
变成了一个单例。
为了更好的理解上述代码,有以下几点需要说明:
- 执行
@singleton
时,# 1
,# 2
,# 3
处的代码立即被执行,这也是为什么上述运行结果会先打印出30个-
符号; - 当通过
TheOne()
的方式创建实例对象时,由于装饰器的作用,此时TheOne()
等价于wrapper_singleton()
:- 当第一次创建实例时,由于
if
条件判断为真,则创建实例,且该实例的引用赋给了wrapper_singleton.instance
; - 当后续再创建实例时,由于
if
条件判断为假,则直接返回属性wrapper_singleton.instance
指向的实例对象引用。
- 当第一次创建实例时,由于
3. 缓存函数返回值
装饰器可以提供优秀的缓存机制。为进行具体阐述,下面代码先通过递归方式定义了一个斐波那契数列函数,并使用类CountCalls作为装饰器统计求取指定序号数列的值时斐波那契数列函数被调用的次数:
import functools
class CountCalls(object):
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print(f"Call {self.num_calls} of {self.func.__name__!r}")
return self.func(*args, **kwargs)
@CountCalls # fibonacci = CountCalls(fibonacci)
def fibonacci(num):
if num < 2:
return num # 当num = 0时,fibonacci(0) = 0;当num = 1时,fibonacci(1) = 1;当num = 2时,fibonacci(2) = 1
return fibonacci(num - 1) + fibonacci(num - 2)
def main():
print(fibonacci(5))
if __name__ == '__main__':
main()
上述代码的运行结果为:
Call 1 of ‘fibonacci’
Call 2 of ‘fibonacci’
Call 3 of ‘fibonacci’
Call 4 of ‘fibonacci’
Call 5 of ‘fibonacci’
Call 6 of ‘fibonacci’
Call 7 of ‘fibonacci’
Call 8 of ‘fibonacci’
Call 9 of ‘fibonacci’
Call 10 of ‘fibonacci’
Call 11 of ‘fibonacci’
Call 12 of ‘fibonacci’
Call 13 of ‘fibonacci’
Call 14 of ‘fibonacci’
Call 15 of ‘fibonacci’
5
即为了计算斐波那契数列的第5项,上述fibonacci()函数被调用了15次,当计算第10项时,调用次数到了177,再当计算第15项时,调用次数竟然增加至了1973次,造成如此快速地次数增加,是因为上述代码每次都会重新递归计算所有前序项,如下图所示:
下面通过对前序计算得出的数列项进行缓存来改善上面代码的弊端:
import functools
class CountCalls(object):
def __init__(self, func): # 1.2 func = fibonacci
functools.update_wrapper(self, func)
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs): # 5
self.num_calls += 1
print(f"Call {self.num_calls} of {self.func.__name__!r}")
return self.func(*args, **kwargs) # 6
def cache(func): # 2.2 此时func = CountCalls(fibonacci)
"""缓存已计算过的斐波那契数列项的值"""
@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) # 4.CountCalls(fibonacci)(*args, **kwargs)
return wrapper_cache.cache[cache_key]
wrapper_cache.cache = dict()
return wrapper_cache
@cache # 2.1 fibonacci = cache(CountCalls(fibonacci)) --> fibonacci = wrapper_cache
@CountCalls # 1.1 fibonacci = CountCalls(fibonacci)
def fibonacci(num):
if num < 2:
return num # 当num = 0时,fibonacci(0) = 0;当num = 1时,fibonacci(1) = 1;当num = 2时,fibonacci(2) = 1
return fibonacci(num - 1) + fibonacci(num - 2)
def main():
print(fibonacci(5)) # 3. wrapper_cache(5)
if __name__ == '__main__':
main()