返回介绍

7.8 标准库中的装饰器

发布于 2024-02-05 21:59:47 字数 7619 浏览 0 评论 0 收藏 0

Python 内置了三个用于装饰方法的函数:property、classmethod 和 staticmethod。property 在 19.2 节讨论,另外两个在 9.4 节讨论。

另一个常见的装饰器是 functools.wraps,它的作用是协助构建行为良好的装饰器。我们在示例 7-17 中用过。标准库中最值得关注的两个装饰器是 lru_cache 和全新的 singledispatch(Python 3.4 新增)。这两个装饰器都在 functools 模块中定义。接下来分别讨论它们。

7.8.1 使用functools.lru_cache做备忘

functools.lru_cache 是非常实用的装饰器,它实现了备忘(memoization)功能。这是一项优化技术,它把耗时的函数的结果保存起来,避免传入相同的参数时重复计算。LRU 三个字母是“Least Recently Used”的缩写,表明缓存不会无限制增长,一段时间不用的缓存条目会被扔掉。

生成第 n 个斐波纳契数这种慢速递归函数适合使用 lru_cache,如示例 7-18 所示。

示例 7-18 生成第 n 个斐波纳契数,递归方式非常耗时

from clockdeco import clock

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

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

运行 fibo_demo.py 得到的结果如下。除了最后一行,其余输出都是 clock 装饰器生成的。

$ python3 fibo_demo.py
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00007892s] fibonacci(2) -> 1
[0.00000095s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00003815s] fibonacci(2) -> 1
[0.00007391s] fibonacci(3) -> 2
[0.00018883s] fibonacci(4) -> 3
[0.00000000s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000119s] fibonacci(1) -> 1
[0.00004911s] fibonacci(2) -> 1
[0.00009704s] fibonacci(3) -> 2
[0.00000000s] fibonacci(0) -> 0
[0.00000000s] fibonacci(1) -> 1
[0.00002694s] fibonacci(2) -> 1
[0.00000095s] fibonacci(1) -> 1
[0.00000095s] fibonacci(0) -> 0
[0.00000095s] fibonacci(1) -> 1
[0.00005102s] fibonacci(2) -> 1
[0.00008917s] fibonacci(3) -> 2
[0.00015593s] fibonacci(4) -> 3
[0.00029993s] fibonacci(5) -> 5
[0.00052810s] fibonacci(6) -> 8
8

浪费时间的地方很明显:fibonacci(1) 调用了 8 次,fibonacci(2) 调用了 5 次……但是,如果增加两行代码,使用 lru_cache,性能会显著改善,如示例 7-19 所示。

示例 7-19 使用缓存实现,速度更快

import functools

from clockdeco import clock

@functools.lru_cache() # ➊
@clock  # ➋
def fibonacci(n):
  if n < 2:
    return n
  return fibonacci(n-2) + fibonacci(n-1)

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

❶ 注意,必须像常规函数那样调用 lru_cache。这一行中有一对括号:@functools.lru_cache()。这么做的原因是,lru_cache 可以接受配置参数,稍后说明。

❷ 这里叠放了装饰器:@lru_cache() 应用到 @clock 返回的函数上。

这样一来,执行时间减半了,而且 n 的每个值只调用一次函数:

$ python3 fibo_demo_lru.py
[0.00000119s] fibonacci(0) -> 0
[0.00000119s] fibonacci(1) -> 1
[0.00010800s] fibonacci(2) -> 1
[0.00000787s] fibonacci(3) -> 2
[0.00016093s] fibonacci(4) -> 3
[0.00001216s] fibonacci(5) -> 5
[0.00025296s] fibonacci(6) -> 8

在计算 fibonacci(30) 的另一个测试中,示例 7-19 中的版本在 0.0005 秒内调用了 31 次 fibonacci 函数,而示例 7-18 中未缓存的版本调用 fibonacci 函数 2 692 537 次,在使用 Intel Core i7 处理器的笔记本电脑中耗时 17.7 秒。

除了优化递归算法之外,lru_cache 在从 Web 中获取信息的应用中也能发挥巨大作用。

特别要注意,lru_cache 可以使用两个可选的参数来配置。它的签名是:

functools.lru_cache(maxsize=128, typed=False)

maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的幂。typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。顺便说一下,因为 lru_cache 使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的

接下来讨论吸引人的 functools.singledispatch 装饰器。

7.8.2 单分派泛函数

假设我们在开发一个调试 Web 应用的工具,我们想生成 HTML,显示不同类型的 Python 对象。

我们可能会编写这样的函数:

import html

def htmlize(obj):
  content = html.escape(repr(obj))
  return '<pre>{}</pre>'.format(content)

这个函数适用于任何 Python 类型,但是现在我们想做个扩展,让它使用特别的方式显示某些类型。

str:把内部的换行符替换为 '<br>\n';不使用 <pre>,而是使用 <p>。

int:以十进制和十六进制显示数字。

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

我们想要的行为如示例 7-20 所示。

示例 7-20 生成 HTML 的 htmlize 函数,调整了几种对象的输出

>>> htmlize({1, 2, 3})  ➊
'<pre>{1, 2, 3}</pre>'
>>> htmlize(abs)
'<pre><built-in function abs></pre>'
>>> htmlize('Heimlich & Co.\n- a game')  ➋
'<p>Heimlich & Co.<br>\n- a game</p>'
>>> htmlize(42)  ➌
'<pre>42 (0x2a)</pre>'
>>> print(htmlize(['alpha', 66, {3, 2, 1}]))  ➍
<ul>
<li><p>alpha</p></li>
<li><pre>66 (0x42)</pre></li>
<li><pre>{1, 2, 3}</pre></li>
</ul>

❶ 默认情况下,在 <pre></pre> 中显示 HTML 转义后的对象字符串表示形式。

❷ 为 str 对象显示的也是 HTML 转义后的字符串表示形式,不过放在 <p></p> 中,而且使用 <br> 表示换行。

❸ int 显示为十进制和十六进制两种形式,放在 <pre></pre> 中。

❹ 各个列表项目根据各自的类型格式化,整个列表则渲染成 HTML 列表。

因为 Python 不支持重载方法或函数,所以我们不能使用不同的签名定义 htmlize 的变体,也无法使用不同的方式处理不同的数据类型。在 Python 中,一种常见的做法是把 htmlize 变成一个分派函数,使用一串 if/elif/elif,调用专门的函数,如 htmlize_str、htmlize_int,等等。这样不便于模块的用户扩展,还显得笨拙:时间一长,分派函数 htmlize 会变得很大,而且它与各个专门函数之间的耦合也很紧密。

Python 3.4 新增的 functools.singledispatch 装饰器可以把整体方案拆分成多个模块,甚至可以为你无法修改的类提供专门的函数。使用 @singledispatch 装饰的普通函数会变成泛函数(generic function):根据第一个参数的类型,以不同方式执行相同操作的一组函数。4 具体做法参见示例 7-21。

4这才称得上是单分派。如果根据多个参数选择专门的函数,那就是多分派了。

 functools.singledispatch 是 Python 3.4 增加的,PyPI 中的 singledispatch可以向后兼容 Python 2.6 到 Python 3.3。

示例 7-21 singledispatch 创建一个自定义的 htmlize.register 装饰器,把多个函数绑在一起组成一个泛函数

from functools import singledispatch
from collections import abc
import numbers
import html

@singledispatch  ➊
def htmlize(obj):
  content = html.escape(repr(obj))
  return '<pre>{}</pre>'.format(content)

@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)

@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>'

❶ @singledispatch 标记处理 object 类型的基函数。

❷ 各个专门函数使用 @«base_function».register(«type») 装饰。

❸ 专门函数的名称无关紧要;_ 是个不错的选择,简单明了。

❹ 为每个需要特殊处理的类型注册一个函数。numbers.Integral 是 int 的虚拟超类。

❺ 可以叠放多个 register 装饰器,让同一个函数支持不同类型。

只要可能,注册的专门函数应该处理抽象基类(如 numbers.Integral 和 abc.MutableSequence),不要处理具体实现(如 int 和 list)。这样,代码支持的兼容类型更广泛。例如,Python 扩展可以子类化 numbers.Integral,使用固定的位数实现 int 类型。

 使用抽象基类检查类型,可以让代码支持这些抽象基类现有和未来的具体子类或虚拟子类。抽象基类的作用和虚拟子类的概念在第 11 章讨论。

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

singledispatch 是经过深思熟虑之后才添加到标准库中的,它提供的特性很多,这里无法一一说明。这个机制最好的文档是“PEP 443 — Single-dispatch generic functions”。

 @singledispatch 不是为了把 Java 的那种方法重载带入 Python。在一个类中为同一个方法定义多个重载变体,比在一个函数中使用一长串 if/elif/elif/elif 块要更好。但是这两种方案都有缺陷,因为它们让代码单元(类或函数)承担的职责太多。@singledispath 的优点是支持模块化扩展:各个模块可以为它支持的各个类型注册一个专门函数。

装饰器是函数,因此可以组合起来使用(即,可以在已经被装饰的函数上应用装饰器,如示例 7-21 所示)。下一节说明其中的原理。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文