- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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.3 重载向量加法运算符 +
Vector 类是序列类型,按照“Data Model”一章中的“3.3.6. Emulating container types”一节所说,序列应该支持 + 运算符(用于拼接),以及 * 运算符(用于重复复制)。然而,我们将使用向量数学运算实现 + 和 * 运算符。这么做更难一些,但是对 Vector 类型来说更有意义。
两个欧几里得向量加在一起得到的是一个新向量,它的各个分量是两个向量中相应的分量之和。比如说:
>>> v1 = Vector([3, 4, 5]) >>> v2 = Vector([6, 7, 8]) >>> v1 + v2 Vector([9.0, 11.0, 13.0]) >>> v1 + v2 == Vector([3+6, 4+7, 5+8]) True
如果尝试把两个不同长度的 Vector 实例加在一起会怎样?此时可以抛出错误,但是根据实际运用情况(例如信息检索),最好使用零填充较短的那个向量。我们想要的效果是这样:
>>> v1 = Vector([3, 4, 5, 6]) >>> v3 = Vector([1, 2]) >>> v1 + v3 Vector([4.0, 6.0, 5.0, 6.0])
确定这些基本的要求之后,__add__ 方法的实现短小精悍,如示例 13-4 所示。
示例 13-4 Vector.__add__ 方法,第 1 版
# 在Vector类中定义 def __add__(self, other): pairs = itertools.zip_longest(self, other, fillvalue=0.0) # ➊ return Vector(a + b for a, b in pairs) # ➋
❶ pairs 是个生成器,它会生成 (a, b) 形式的元组,其中 a 来自 self,b 来自 other。如果 self 和 other 的长度不同,使用 fillvalue 填充较短的那个可迭代对象。
❷ 构建一个新 Vector 实例,使用生成器表达式计算 pairs 中各个元素的和。
注意,__add__ 返回一个新 Vector 实例,而没有影响 self 或 other。
实现一元运算符和中缀运算符的特殊方法一定不能修改操作数。使用这些运算符的表达式期待结果是新对象。只有增量赋值表达式可能会修改第一个操作数(self),参见 13.6 节。
示例 13-4 中的实现方式可以把 Vector 加到 Vector2d 上,还可以把 Vector 加到元组或任何生成数字的可迭代对象上,如示例 13-5 所示。
示例 13-5 第 1 版 Vector.__add__ 方法也支持 Vector 之外的对象
>>> v1 = Vector([3, 4, 5]) >>> v1 + (10, 20, 30) Vector([13.0, 24.0, 35.0]) >>> from vector2d_v3 import Vector2d >>> v2d = Vector2d(1, 2) >>> v1 + v2d Vector([4.0, 6.0, 5.0])
示例 13-5 中的两个加法都能如我们所期待的那样计算,这是因为 __add__ 使用了 zip_longest(...),它能处理任何可迭代对象,而且构建新 Vector 实例的生成器表达式仅仅是把 zip_longest(...) 生成的值对相加(a + b),因此可以使用任何生成数字元素的可迭代对象。
然而,如果对调操作数(见示例 13-6),混合类型的加法就会失败。
示例 13-6 如果左操作数是 Vector 之外的对象,第一版 Vector.__add__ 方法无法处理
>>> v1 = Vector([3, 4, 5]) >>> (10, 20, 30) + v1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can only concatenate tuple (not "Vector") to tuple >>> from vector2d_v3 import Vector2d >>> v2d = Vector2d(1, 2) >>> v2d + v1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'Vector2d' and 'Vector'
为了支持涉及不同类型的运算,Python 为中缀运算符特殊方法提供了特殊的分派机制。对表达式 a + b 来说,解释器会执行以下几步操作(见图 13-1)。
(1) 如果 a 有 __add__ 方法,而且返回值不是 NotImplemented,调用 a.__add__(b),然后返回结果。
(2) 如果 a 没有 __add__ 方法,或者调用 __add__ 方法返回 NotImplemented,检查 b 有没有 __radd__ 方法,如果有,而且没有返回 NotImplemented,调用 b.__radd__(a),然后返回结果。
(3) 如果 b 没有 __radd__ 方法,或者调用 __radd__ 方法返回 NotImplemented,抛出 TypeError,并在错误消息中指明操作数类型不支持。
图 13-1:使用 __add__ 和 __radd__ 计算 a + b 的流程图
__radd__ 是 __add__ 的“反射”(reflected)版本或“反向”(reversed)版本。我喜欢把它叫作“反向”特殊方法。4 本书的三位技术审校,Alex、Anna 和 Leo 告诉我,他们喜欢称之为“右向”(right)特殊方法,因为他们在右操作数上调用。不管你喜欢哪个以“r”开头的单词, __radd__ 和 __rsub__ 等类似方法中的“r”就是这个意思。
4这两个术语在 Python 文档中都使用过。“Data Model”一章用的是“reflected”(反射),而 numbers 模块文档的“9.1.2.2. Implementing the arithmetic operations”一节用的是“forward”(正向)方法和“reverse”(反向)方法。我觉得后者更好,因为“正向”和“反向”明确指出了方向,而“反射”就没这种效果。
因此,为了让示例 13-6 中的混合类型加法能正确计算,我们要实现 Vector.__radd__ 方法。这是一种后备机制,如果左操作数没有实现 __add__ 方法,或者实现了,但是返回 NotImplemented 表明它不知道如何处理右操作数,那么 Python 会调用 __radd__ 方法。
别把 NotImplemented 和 NotImplementedError 搞混了。前者是特殊的单例值,如果中缀运算符特殊方法不能处理给定的操作数,那么要把它返回(return)给解释器。而 NotImplementedError 是一种异常,抽象类中的占位方法把它抛出(raise),提醒子类必须覆盖。
最简可用的 __radd__ 实现如示例 13-7 所示。
示例 13-7 Vector.__add__ 和 __radd__ 方法
# 在Vector类中定义 def __add__(self, other): # ➊ pairs = itertools.zip_longest(self, other, fillvalue=0.0) return Vector(a + b for a, b in pairs) def __radd__(self, other): # ➋ return self + other
❶ __add__ 方法与示例 13-4 中一样,没有变化;这里列出,是因为 __radd__ 要用它。
❷ __radd__ 直接委托 __add__。
__radd__ 通常就这么简单:直接调用适当的运算符,在这里就是委托 __add__。任何可交换的运算符都能这么做。处理数字和向量时,+ 可以交换,但是拼接序列时不行。
示例 13-4 中的方法可以处理 Vector 对象或任何具有数值元素的可迭代对象,例如 Vector2d 实例、整数元组或浮点数数组。但是,如果提供的对象不可迭代,那么 __add__ 就无法处理,而且提供的错误消息不是很有用,如示例 13-8 所示。
示例 13-8 Vector.__add__ 方法的操作数要是可迭代对象
>>> v1 + 1 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "vector_v6.py", line 328, in __add__ pairs = itertools.zip_longest(self, other, fillvalue=0.0) TypeError: zip_longest argument #2 must support iteration
如果一个操作数是可迭代对象,但是它的元素不能与 Vector 中的浮点数元素相加,给出的消息也没什么用。如示例 13-9 所示。
示例 13-9 Vector.__add__ 方法的操作数应是可迭代的数值对象
>>> 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'
示例 13-8 和示例 13-9 揭露的问题比晦涩难懂的错误消息严重:如果由于类型不兼容而导致运算符特殊方法无法返回有效的结果,那么应该返回 NotImplemented,而不是抛出 TypeError。返回 NotImplemented 时,另一个操作数所属的类型还有机会执行运算,即 Python 会尝试调用反向方法。
为了遵守鸭子类型精神,我们不能测试 other 操作数的类型,或者它的元素的类型。我们要捕获异常,然后返回 NotImplemented。如果解释器还未反转操作数,那么它将尝试去做。如果反向方法返回 NotImplemented,那么 Python 会抛出 TypeError,并返回一个标准的错误消息,例如“unsupported operand type(s) for +: Vector and str”。
示例 13-10 是实现 Vector 加法的特殊方法的最终版。
示例 13-10 vector_v6.py:+ 运算符方法,添加到 vector_v5.py(见示例 10-16)中
def __add__(self, other): try: pairs = itertools.zip_longest(self, other, fillvalue=0.0) return Vector(a + b for a, b in pairs) except TypeError: return NotImplemented def __radd__(self, other): return self + other
如果中缀运算符方法抛出异常,就终止了运算符分派机制。对 TypeError 来说,通常最好将其捕获,然后返回 NotImplemented。这样,解释器会尝试调用反向运算符方法,如果操作数是不同的类型,对调之后,反向运算符方法可能会正确计算。
至此,我们编写了 __add__ 和 __radd__ 方法,安全重载了 + 运算符。接下来实现另一个中缀运算符:*。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论