标准库中让人印象深刻的装饰器:Cache和单分派

使用functools.cache做备忘

functools.cache装饰器实现了备忘。这是一项优化技术,能把耗时的函数得到的结果保存起来,避免传入相同参数时重复计算。

functools.cache是Python3.9新增的,下面通过斐波那契这种慢递归的函数适合使用@cache。

import functools
import time

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = [repr(arg) for arg in args]
        arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
        arg_str = ', '.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) ->{result!r}')
        return result

    return clocked

@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

if __name__ == '__main__':
    fibonacci(6)

运行结果如下所示:

[0.00000070s] fibonacci(0) ->0
[0.00000300s] fibonacci(1) ->1
[0.00522990s] fibonacci(2) ->1
[0.00000020s] fibonacci(1) ->1
[0.00000030s] fibonacci(0) ->0
[0.00000030s] fibonacci(1) ->1
[0.00001490s] fibonacci(2) ->1
[0.00002960s] fibonacci(3) ->2
[0.00527690s] fibonacci(4) ->3
[0.00000020s] fibonacci(1) ->1
[0.00000020s] fibonacci(0) ->0
[0.00000030s] fibonacci(1) ->1
[0.00001410s] fibonacci(2) ->1
[0.00002800s] fibonacci(3) ->2
[0.00000020s] fibonacci(0) ->0
[0.00000020s] fibonacci(1) ->1
[0.00001390s] fibonacci(2) ->1
[0.00000020s] fibonacci(1) ->1
[0.00000030s] fibonacci(0) ->0
[0.00000020s] fibonacci(1) ->1
[0.00001450s] fibonacci(2) ->1
[0.00002870s] fibonacci(3) ->2
[0.00005630s] fibonacci(4) ->3
[0.00009810s] fibonacci(5) ->5
[0.00539110s] fibonacci(6) ->8

 浪费时间的地方很明显:fibonacci(1)调用了8次,fibonacci(2)调用了5次...,但是如果使用cache,性能将得到显著改善。如下所示:

import functools
import time

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = [repr(arg) for arg in args]
        arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
        arg_str = ', '.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) ->{result!r}')
        return result

    return clocked

@functools.cache
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

if __name__ == '__main__':
    fibonacci(6)

输出结果:

[0.00000060s] fibonacci(0) ->0
[0.00000060s] fibonacci(1) ->1
[0.00005630s] fibonacci(2) ->1
[0.00000080s] fibonacci(3) ->2
[0.00007680s] fibonacci(4) ->3
[0.00000060s] fibonacci(5) ->5
[0.00009410s] fibonacci(6) ->8

是不是对于每个n值,fibonacci函数只调用1次。注意的是,被装饰的函数必须是可哈希的,因为底层lru_cache使用dict存储结果,字典的键取自传入的位置参数和关键字参数。还有就是,如果缓存较大则@cache有可能耗尽所有可用内存。对于长期运行的进程,推荐使用@lru_cache,并合理设置maxsize参数。

使用lru_cache

上面讲的functools.cache只是对交旧的functools.lru_cache函数的简单包装。其实,lru_cache更灵活。

@lru_cache的优势主要在于可以通过maxsize参数限制内存用量上限。maxsize参数的默认值相当保守,只有128,即缓存最多只有128条。

用法为:

@functools.lru_cache(maxsize=2**20, typed=True)
@clock
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

采用下述默认参数。

maxsize:设定最多可以存储多少条目。缓存满了以后,最不常用的条目会被丢弃,为新条目腾出空间。为了得到最佳性能,应将maxsize设为2的次方。如果传入maxsize=None,则LRU逻辑将被彻底禁用,因此缓存速度更快,但是条目永远不会被丢弃,这可能会被消耗更多内存。

typed=False:决定是否把不同参数类型得到的结果分开保存。例如,在默认设置下,被认为是值相等的浮点数和整数参数只存储一次,即f(1)调用和f(1.0)调用只对应一个缓存条目。如果设为True,则在不同的条目中存储可能不一样的结果。

单分派泛化函数

假设我在开发一个调试web应用程序的工具,想生成HTML,以显示不同类型的Python对象。我们可以这样编写代码:

import html
def htmlize(obj):
    content = html.escape(repr(obj))
    return f"<pre>{content}</pre>"

但是,这还不能满足我的需求,我想把它扩展一下,以特殊的方式达到以下类型:

str:把字符串中的换行符替换成‘<br/>\n’,不使用<pre>,而使用<p>

int:以十进制和十六进制显示数(bool除外)

list:输出一个HTML列表,根据各项的类型进行格式化

这时我们可以使用单分派函数:functools.singledispatch装饰器可以把整体方案拆分成多个模块,甚至可以为第三方包中无法编辑的类型提供专门的函数。使用singledispatch装饰的普通函数变成了泛化函数(指根据第一个参数的类型,以不同的方式执行相同操作的一组函数)的入口。这就是单分派。如果根据多个参数选择专门的函数,那就是多分派。代码如下所示:

import functools
import html
import numbers
from collections import abc

@functools.singledispatch
def htmlize(obj):
    content = html.escape(repr(obj))
    return f"<pre>{content}</pre>"

@htmlize.register
def _(text: str) -> str:
    content = html.escape(text).replace('\n', '<br/>\n')
    return f"<p>{content}</p>"

@htmlize.register
def _(n: numbers.Integral) -> str:
    return f"<pre>{n}(0x{n:x})</pre>"

@htmlize.register(bool) #这种写法也可以
def _(b) -> str:
    return f"<pre>{b}</pre>"

@htmlize.register
def _(seq: abc.Sequence) -> str:
    inner = '</li>\n<li>'.join(htmlize(i) for i in seq)
    return f'<ul>\n<li>{inner}</li>\n</ul>'


print(htmlize({1, 2, 3}))  # <pre>{1, 2, 3}</pre>
print(htmlize("sdhnfjnjs\nkj"))  # <p>sdhnfjnjs<br/>\nkj</p>
# <ul>
# <li><p>jay</p></li>
# <li><pre>66(0x42)</pre></li>
# <li><pre>{1, 2, 3}</pre></li>
# </ul>
print(htmlize(['jay', 66, {1, 2, 3}]))
print(htmlize(True))  #<pre>True</pre>

解释一下上面代码:@functools.singledispatch标记的是处理object类型的基函数。各个专门函数使用@base.register装饰。运行时传入的一个参数类型决定何时使用这个函数,专门函数的名称无关紧要,所以用_表示。为每个需要特殊处理的类型注册一个函数,把一个参数的类型提示设为相应的类型。如果不想或者不能为被装饰器的类型添加类型提示,则可以把类型传给register装饰器,像代码中处理bool类型是一样。

singledispatch机制一个显著特征是,你可以在系统的任何地方和任何模块中注册专门的函数。如果后来在新模块中定义了新类型,则可以轻易添加一个新的自定义函数来处理新类型。此外,还可以为不是自己编写或者不能修改的类编写自定义函数。

singledispatch功能很多,感兴趣的可以去看看官方文档哈。

 

  • 40
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值