- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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 术语表
- 作者简介
- 关于封面
10.9 延伸阅读
Vector 类中的大多数特殊方法在第 9 章定义的 Vector2d 类中也有,因此前一章给出的延伸阅读材料同样适合本章。
强大的高阶函数 reduce 也叫合拢、累计、聚合、压缩和注入。更多信息参见维基百科中的“Fold (higher-order function)”词条。这篇文章展示了高阶函数的用途,着重说明了具有递归数据结构的函数式语言。这篇文章中还有一个表格,列出了很多编程语言中起合拢作用的函数。
杂谈
把协议当作非正式的接口
协议不是 Python 发明的。Smalltalk 团队,也就是“面向对象”的发明者,使用“协议”这个词表示现在我们称之为接口的特性。某些 Smalltalk 编程环境允许程序员把一组方法标记为协议,但这只不过是一种文档,用于辅助导航,语言不对其施加特定措施。因此,向熟悉正式(而且编译器会施加措施)接口的人解释“协议”时,我会简单地说它是“非正式的接口”。
动态类型语言中的既定协议会自然进化。所谓动态类型是指在运行时检查类型,因为方法签名和变量没有静态类型信息。Ruby 是一门重要的面向对象动态类型语言,它也使用协议。
在 Python 文档中,如果看到“文件类对象”这样的表述,通常说的就是协议。这是一种简短的说法,意思是:“行为基本与文件一致,实现了部分文件接口,满足上下文相关需求的东西。”
你可能觉得只实现协议的一部分不够严谨,但是这样做的优点是简单。“Data Model”一章的 3.3 节建议:
模仿内置类型实现类时,记住一点:模仿的程度对建模的对象来说合理即可。例如,有些序列可能只需要获取单个元素,而不必提取切片。
——Python 语言参考手册中“Data Model”一章
不要为了满足过度设计的接口契约和让编译器开心,而去实现不需要的方法,我们要遵守 KISS 原则。
第 11 章还会讨论协议和接口,这正是那一章的主要话题。
鸭子类型的起源
我相信 Ruby 社区在“鸭子类型”这个术语的推广过程中起了主要作用,因为他们向大量 Java 使用者宣扬了这个说法。但是,在 Ruby 或 Python 流行起来之前,Python 就使用这个术语了。根据维基百科,在面向对象编程中较早使用鸭子作比喻的人是 Alex Martelli,在他于 2000 年 7 月 26 日发到 Python-list 中的一篇文章里:“polymorphism (was Re: Type checking in python?)”。本章开头引用的那句话就出自那篇文章。如果你想知道“鸭子类型”这个术语的真正起源,以及很多编程语言对这个面向对象概念的运用,请阅读维基百科中的“Duck typing”词条。
安全的 __format__ 方法,增强可用性
实现 __format__ 方法时,我们没有采取措施防范 Vector 实例拥有大量分量,不过在 __repr__ 方法中我们使用 reprlib 做了预防。这是因为 repr() 函数用于调试和记录日志,所以必须生成可用的输出;而 __format__ 方法用于向最终用户显示输出,他们大概想看到整个 Vector。如果你觉得这样做危险,可以再为格式规范微语言实现一个扩展。
如果是我,我会这么做:默认情况下,格式化的 Vector 实例显示有限个分量,比如说 30 个。如果元素数量超过上限,默认的行为是像 reprlib 那样,截断超出的部分,使用 ... 表示。然而,如果格式说明符后面有特殊的 * 代码(意思是“全部”),那么就不限制显示的元素数量。因此,用户在不知情的情况下不会被特别长的输出吓到。如果默认的上限碍事,那么 ... 的存在对用户是个提醒,用户研究文档后会发现 * 格式代码。
如果你实现了,请向本书的 GitHub 仓库发一个拉取请求。
寻找符合 Python 风格的求和方式
就像“什么是美”没有确切的答案一样,“什么是 Python 风格”也没有标准答案。如果回答“地道的 Python”(我通常会这样说),不能让人 100% 满意,因为对你来说是“地道的”,在我看来却可能不是。但我可以肯定的是,“地道”并不是指使用最鲜为人知的语言特性。
Python-list 中有一篇发表于 2003 年 4 月的话题,题为“Pythonic Way to Sum n-th List Element?”。这个话题与本章讨论的 reduce 函数有关。
该话题的发起人 Guy Middleton 说他不喜欢使用 lambda 表达式,问下面这个方案有没有办法改进:9
>>> my_list = [[1, 2, 3], [40, 50, 60], [9, 8, 7]] >>> import functools >>> functools.reduce(lambda a, b: a+b, [sub[1] for sub in my_list]) 60
这段代码有很多习惯用法:lambda、reduce 和列表推导。最终,这可能会变成人气竞赛,因为它冒犯了讨厌 lambda 的人和看不上列表推导的人——这两种人都很多。
如果使用 lambda,或许就不应该使用列表推导——过滤除外,但这不是过滤。
下面是我给出的方案,这能讨得 lambda 拥护者的欢心:
>>> functools.reduce(lambda a, b: a + b[1], my_list, 0) 60
我没有参与那个话题,而且我不会在真实的代码中使用上述方案,因为我非常不喜欢 lambda 表达式。这里只是为了举例说明不使用列表推导怎么做。
第一个答案是 Fernando Perez 给出的,他是 IPython 的创建者,他的答案强调了 NumPy 支持 n 维数组和 n 维切片:
>>> import numpy as np >>> my_array = np.array(my_list) >>> np.sum(my_array[:, 1]) 60
我觉得 Perez 的方案很棒,不过 Guy Middleton 推崇 Paul Rubin 和 Skip Montanaro 给出的下述方案:
>>> import operator >>> functools.reduce(operator.add, [sub[1] for sub in my_list], 0) 60
随后,Evan Simpson 问道:“这样做有什么错?”
>>> total = 0 >>> for sub in my_list: ... total += sub[1] >>> total 60
许多人都觉得这也很符合 Python 风格。Alex Martelli 甚至说,Guido 或许就会这么做。
我喜欢 Evan Simpson 的代码,不过也喜欢 David Eppstein 对此给出的评论:
如果你想计算列表中各个元素的和,写出的代码应该看起来像是在“计算元素之和”,而不是“迭代元素,维护一个变量 t,再执行一系列求和操作”。如果不能站在一定高度上表明意图,让语言去关注低层操作,那么要高级语言干嘛?
之后 Alex Martelli 又建议:
求和操作经常需要,我不介意 Python 提供一个这样的内置函数。但是,在我看来,“reduce(operator.add, ...”不是好方法(作为一名 APL 老程序员和 FP 语言的爱好者,我应该喜欢,但是我并不喜欢)。
随后,Alex 建议提供并实现了 sum() 函数。这次讨论之后三个月,Python 2.3 就内置了这个函数。因此,Alex 喜欢的句法变成了标准:
>>> sum([sub[1] for sub in my_list]) 60
下一年年末(2004 年 11 月),Python 2.4 发布了,这一版引入了生成器表达式。因此,在我看来,Guy Middleton 那个问题目前最符合 Python 风格的答案是:
>>> sum(sub[1] for sub in my_list) 60
这样写不仅比使用 reduce 函数更易阅读,而且还能避免空序列导致的陷阱:sum([]) 的结果是 0,就这么简单。
在这次讨论中,Alex Martelli 指出,Python 2 内置的 reduce 函数成事不足败事有余,因为它推荐的地道编程方式难以理解。他的观点最有说服力:Python 3 把 reduce 函数移到 functools 模块中了。
当然,functools.reduce 函数仍有它的作用。实现 Vector.__hash__ 方法时我就用了它,我觉得我的实现方式算得上符合 Python 风格。
9为了在此展示,我稍微修改了这段代码,因为在 2003 年,reduce 是内置函数,而在 Python 3 中要导入。此外,我把 x 和 y 换成了 my_list 和 sub(表示子串)。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论