返回介绍

13.3 重载向量加法运算符 +

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

 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 技术交流群。

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

发布评论

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