- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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 术语表
- 作者简介
- 关于封面
13.8 延伸阅读
在 Python 编程中,运算符重载经常使用 isinstance 做测试。一般来说,库应该利用动态类型(提高灵活性),避免显式测试类型,而是直接尝试操作,然后处理异常,这样只要对象支持所需的操作即可,而不必一定是某种类型。但是,Python 抽象基类允许一种更为严格的鸭子类型,Alex Martelli 称之为“白鹅类型”,编写重载运算符的代码时经常能用到。因此,如果你跳过了第 11 章,一定要去读读。
运算符特殊方法的主要参考资料是“Data Model”一章。这是权威资料,不过如“Python 3 文档的缺陷”所述,现在有个明显的缺陷,12 即建议“如果定义 __eq__() 方法,同时也要定义 __ne__() 方法”。实际上,在 Python 3 中,继承自 object 类的 __ne__ 方法能满足绝大多数需求,因此一般很少实现 __ne__ 方法。Python 标准库中 numbers 模块文档的“9.1.2.2. Implementing the arithmetic operations”一节也值得一读。
12这个缺陷现在已经修正了。——编者注
与之相关的一个技术是泛函数,由 Python 3 的 @singledispatch 装饰器支持(参见 7.8.2 节)。在 David Beazley 与 Brian K. Jones 的著作《Python Cookbook(第 3 版)中文版》中,“9.20 通过函数注解来实现方法重载”秘笈使用一些高级元编程(涉及元类)通过函数注解实现了基于类型的分派。Martelli、Ravenscroft 与 Ascher 的《Python Cookbook(第 2 版)中文版》一书有个有趣的诀窍(2.13 节,Erik Max Francis 提供),展示了如何重载 << 运算符,在 Python 中模仿 C++ 的 iostream 句法。这两本书中还有一些其他关于运算符重载的示例,我只提了两个重要的诀窍。
functools.total_ordering 函数是个类装饰器(Python 2.7 及以上版本可用),它能为只定义了几个比较运算符的类自动生成全部比较运算符。请参阅 functools 模块的文档。
如果你对动态类型语言的运算符方法分派机制感兴趣,推荐阅读两篇具有重大意义的论文:Dan Ingalls(Smalltalk 团队的创始成员)写的“A Simple Technique for Handling Multiple Polymorphism”,以及 Kurt J. Hebel 与 Ralph Johnson(Johnson 是《设计模式:可复用面向对象软件的基础》的作者之一,因此出了名)合写的“Arithmetic and Double Dispatching in Smalltalk-80”。这两篇论文深入分析了动态类型语言(如 Smalltalk、Python 和 Ruby)的多态。
Python 没有使用这两篇论文中所述的双重分配处理运算符。Python 使用的正向运算符和反向运算符更便于用户定义的类支持双重分派,但是这种方式需要解释器做些特殊处理。与之相比,经典的双重分派是一般性的技术,Python 和任何面向对象语言都能使用,而且不止适用于中缀运算符。其实,Ingalls、Hebel 和 Johnson 描述双重分派使用的示例完全不同。
本章开篇引用的那段话,以及“杂谈”中引用的两段话,均出自“The C Family of Languages: Interview with Dennis Ritchie, Bjarne Stroustrup, and James Gosling”一文,刊登于 Java Report, 5(7), July 2000 和 C++ Report, 12(7), July/August 2000 上。如果你对编程语言设计感兴趣,那么这篇文章非常值得一读。
杂谈
运算符重载的优缺点
如本章开头引用的那段话所述,James Gosling 决定不让 Java 支持运算符重载。在那次访谈中(“The C Family of Languages: Interview with Dennis Ritchie, Bjarne Stroustrup, and James Gosling”),他说:
大约 20% 到 30% 的人觉得运算符重载是罪恶之源;有些人对运算符的重载惹怒了很多人,因为他们使用 + 做列表插入,导致生活一团糟。这类问题大都源于一个事实:世界上有成千上万个运算符,但是只有少数几个适合重载。因此,我们要挑选,但是有时所作的决定违背直觉。
Guido van Rossum 为运算符重载采取了一种折中方式:不放任用户随意创建运算符,如 <=> 或 :-),这样防止了用户对运算符的异想天开,而且能让 Python 解析器保持简单。此外,Python 还禁止重载内置类型的运算符,这个限制也能增强可读性和可预知的性能。
Gosling 接着说道:
社区中约有 10% 的人能正确地使用和真正关心运算符重载,对这些人来说,运算符重载是极其重要的。这部分人几乎专门处理数字,在这一领域中,为了符合人类的直觉,表示法特别重要,因为他们进入这一领域时,直觉中已经知道 + 的意思,他们知道“a + b”中的 a 和 b 可以是复数、矩阵或其他合理的东西。
表示法方面的问题不能低估。下面以金融领域为例说明。在 Python 中,可以使用下述公式计算复利:
interest = principal * ((1 + rate) ** periods - 1)
不管涉及什么数字类型,这种表示法都成立。因此,如果是做重要的金融工作,你要确保 periods 是整数,rate、interest 和 principal 是精确的数字(Python 中 decimal.Decimal 类的实例),这样上述公式就能完好运行。
但是在 Java 中,如果把 float 换成精度不定的 BigDecimal,就无法再使用中缀运算符,因为中缀运算符只支持基本类型。在 Java 中,支持 BigDecimal 数字的公式要这样写:
BigDecimal interest = principal.multiply(BigDecimal.ONE.add(rate) .pow(periods).subtract(BigDecimal.ONE));
显然,使用中缀运算符的公式更易读,至少对大多数人来说如此。13 为了让中缀运算符表示法支持非基本类型,运算符必须能重载。Python 是门高级语言,易于使用,支持运算符重载可能就是它这些年在科学计算领域得到广泛使用的主要原因。
当然,语言不支持运算符重载也有好处。对极为重视性能和安全的低级系统语言而言,这无疑是正确的决定。新近出现的 Go 语言在这方面效仿了 Java,它不支持运算符重载。
但是,重载的运算符,如果使用得当,的确能让代码更易于阅读和编写。对现代的高级语言来说,这是个好功能。
惰性计算一瞥
如果仔细看示例 13-9 中的调用跟踪,会发现生成器表达式做惰性计算的证据。示例 13-19 再次列出那些调用跟踪,不过加上了一些标注。
示例 13-19 与示例 13-9 一样
>>> v1 + 'ABC' Traceback (most recent call last): File "<stdin>", line 1, in <module> File "vector_v6.py", line 329, in __add__ return Vector(a + b for a, b in pairs) # ➊ File "vector_v6.py", line 243, in __init__ self._components = array(self.typecode, components) # ➋ File "vector_v6.py", line 329, in <genexpr> return Vector(a + b for a, b in pairs) # ➌ TypeError: unsupported operand type(s) for +: 'float' and 'str'
❶ Vector 调用的 components 参数是一个生成器表达式。这一步没问题。
❷ components 生成器表达式传给 array 构造方法。在这里,Python 尝试迭代生成器表达式,因此会计算第一个元素 a + b。这里抛出了 TypeError。
❸ 异常向上冒泡,到达 Vector 构造方法调用,在这里报告出来。这表明,生成器表达式在最后时刻才会计算,而不是在源码中定义它的位置计算。
与之相比,如果像 Vector([a + b for a, b in pairs]) 这样调用 Vector 构造方法,那么这里就会抛出异常,因为列表推导会尝试构建一个列表,以便作为参数传给 Vector() 调用。此时,根本不会触及 Vector.__init__ 的定义体。
第 14 章会详细讨论生成器表达式,但是我不想让示例中偶然出现的惰性计算迹象漏过去。
13我的朋友 Mario Domenech Goulart,CHICKEN Scheme 编译器的核心开发者,可能不会同意这一说法。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论