摘自流畅的Python 第七章 函数装饰器和闭包
使用functools.lru_cache 做备忘
functools.lru_cache 是非常使用的装饰器,它实现了备忘(memoization)功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数是重复计算
clockdeco.py
import time
def clock(func):
def clocked(*args):
t0 = time.time()
result = func(*args)
elapsed = time.time() - t0
name = func.__name__
arg_str = ', '.join(repr(arg) for arg in args)
print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
return result
return clocked
我们使用斐波那契数列作为例子 观察
fibo_demo.py
from clockdeco import clock
import time
@clock
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-2) + fibonacci(n-1)
if __name__=='__main__':
start = time.time()
print(fibonacci(30))
print(time.time() - start)
部分输出如下
...
[0.92167187s] fibonacci(24) -> 46368
[1.41511607s] fibonacci(25) -> 75025
[2.26029801s] fibonacci(26) -> 121393
[3.61707902s] fibonacci(27) -> 196418
[5.80630493s] fibonacci(28) -> 317811
[9.39297986s] fibonacci(29) -> 514229
[15.82301521s] fibonacci(30) -> 832040
832040
15.82302474975586
浪费时间的地方很明显:fibonacci(1) 调用了8次,fibonacci(5)调用了5次…
现在我们在代码中添加lru_cache
fibo_demo_lru.py
import functools
import time
from clockdeco import clock
@functools.lru_cache() # 注意,必须像常规函数那样调用 lru_cache。这一行中有一对括 号:@functools.lru_cache()。这么做的原因是,lru_cache 可以 接受配置参数,稍后说明。
@clock # 这里叠放了装饰器:@lru_cache() 应用到 @clock 返回的函数上。这样一来,执行时间减半了,而且 n 的每个值只调用一次函数:
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 2) + fibonacci(n - 1)
if __name__ == '__main__':
start = time.time()
print(fibonacci(30))
print(time.time() - start)
部分输出如下
[0.00000119s] fibonacci(25) -> 75025
[0.00016594s] fibonacci(26) -> 121393
[0.00000000s] fibonacci(27) -> 196418
[0.00020003s] fibonacci(28) -> 317811
[0.00000072s] fibonacci(29) -> 514229
[0.00020981s] fibonacci(30) -> 832040
832040
0.00021791458129882812
由此可以看出使用lru_cache 优化的斐波那契 在计算的耗时方面极大程度的优化了
特别要注意,lru_cache可以使用两个可选的参数来配置。它的签名是
functools.lru_cache(maxsize=128, typed=False)
maxsize参数指定存储多少个调用的结果。缓存慢了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize应该设为2的幂。
type参数如果设为True,把不同参数类型得到的结果分开保存,即把通常人为相等的浮点数和整数参数(如1和1.0)区分开。顺便说一下,因为lru_cache使用字典存储结果,而且键根据调用是传入的定位参数和关键字参数创建,所以被lru_cache装饰的函数,它的所有参数都必须是可散列的
单分派泛函数 singledispatch(重载)
假设我们在开发一个调试Web应用的工具,我们想生成HTML,显示不同类型的Python对象。我们可能会编写这样的函数
import html
def htmlize(obj):
# repr() 函数将对象转化为供解释器读取的形式
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
print(htmlize({1,2,3}))
这个函数使用与任何Python类型,但是现在我们想做个拓展,让它使用特别的方式显示某些类型
- str: 把内部的换行符替换为’<br>\n’; 不使用<pre>,而是使用<p>
- int: 以十进制和十六进制显示数字
- list: 输出一个HTML列表,根据各个元素的类型进行格式化
<pre>{1, 2, 3}</pre>
因为Python不支持重载方法或函数,所以我们不能使用不同的签名定义htmlize变体,也无法使用不同的方式处理不同的数据类型
generic.py
from functools import singledispatch
from collections import abc
import numbers
import html
# @singledispatch 标记处理 object 类型的基函数。
@singledispatch
def htmlize(obj):
content = html.escape(repr(obj))
return '<pre>{}</pre>'.format(content)
# 各个专门函数使用 @«base_function».register(«type») 装饰。
@htmlize.register(str)
def _(text):
content = html.escape(text).replace('\n', '<br>\n')
return '<p>{0}</p>'.format(content)
@htmlize.register(numbers.Integral)
def _(n):
return '<pre>{0} (0x{0:x})</pre>'.format(n)
# 可以叠放多个 register 装饰器,让同一个函数支持不同类型。
@htmlize.register(tuple)
@htmlize.register(abc.MutableSequence)
def _(seq):
inner = '</li>\n<li>'.join(htmlize(item) for item in seq)
return '<ul>\n<li>' + inner + '</li>\n</ul>'
print(htmlize({1,2,3}))
print('-'*40)
print(htmlize(abs))
print('-'*40)
print(htmlize('Heimlich & Co.\n- a game'))
print('-'*40)
print(htmlize(42))
print('-'*40)
print(htmlize(['alpha', 66, {3, 2, 1}]))
输出如下
<pre>{1, 2, 3}</pre>
----------------------------------------
<pre><built-in function abs></pre>
----------------------------------------
<p>Heimlich & Co.<br>
- a game</p>
----------------------------------------
<pre>42 (0x2a)</pre>
----------------------------------------
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>