Python funtools 中的 lru_cache 和 singledispatch

摘自流畅的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>&lt;built-in function abs&gt;</pre>
----------------------------------------
<p>Heimlich &amp; 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>

上一篇:146. LRU 缓存


下一篇:由浅入深带你手写LRU