- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
7.8 标准库中的装饰器
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论