使用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功能很多,感兴趣的可以去看看官方文档哈。