Vector 类已经支持增量赋值运算符 += 和 *= 了,如示例 13-15 所示。
示例 13-15 增量赋值不会修改不可变目标,而是新建实例,然后重新绑定
>>> v1 = Vector([1, 2, 3]) >>> v1_alias = v1 # ➊ >>> id(v1) # ➋ 4302860128 >>> v1 += Vector([4, 5, 6]) # ➌ >>> v1 # ➍ Vector([5.0, 7.0, 9.0]) >>> id(v1) # ➎ 4302859904 >>> v1_alias # ➏ Vector([1.0, 2.0, 3.0]) >>> v1 *= 11 # ➐ >>> v1 # ➑ Vector([55.0, 77.0, 99.0]) >>> id(v1) 4302858336
❶ 复制一份,供后面审查 Vector([1, 2, 3]) 对象。
❷ 记住一开始绑定给 v1 的 Vector 实例的 ID。
❸ 增量加法运算。
❹ 结果与预期相符……
❺ ……但是创建了新的 Vector 实例。
❻ 审查 v1_alias,确认原来的 Vector 实例没被修改。
❼ 增量乘法运算。
❽ 同样,结果与预期相符,但是创建了新的 Vector 实例。
如果一个类没有实现表 13-1 列出的就地运算符,增量赋值运算符只是语法糖:a += b 的作用与 a = a + b 完全一样。对不可变类型来说,这是预期的行为,而且,如果定义了 __add__ 方法的话,不用编写额外的代码,+= 就能使用。
然而,如果实现了就地运算符方法,例如 __iadd__,计算 a += b 的结果时会调用就地运算符方法。这种运算符的名称表明,它们会就地修改左操作数,而不会创建新对象作为结果。
不可变类型,如
Vector类,一定不能实现就地特殊方法。这是明显的事实,不过还是值得提出来。
为了展示如何实现就地运算符,我们将扩展示例 11-12 中的 BingoCage 类,实现 __add__ 和 __iadd__ 方法。
我们把子类命名为 AddableBingoCage。示例 13-16 是我们想让 + 运算符具有的行为。
示例 13-16 使用
+运算符新建AddableBingoCage实例
>>> vowels = 'AEIOU'
>>> globe = AddableBingoCage(vowels) ➊
>>> globe.inspect()
('A', 'E', 'I', 'O', 'U')
>>> globe.pick() in vowels ➋
True
>>> len(globe.inspect()) ➌
4
>>> globe2 = AddableBingoCage('XYZ') ➍
>>> globe3 = globe + globe2
>>> len(globe3.inspect()) ➎
7
>>> void = globe + [10, 20] ➏
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +: 'AddableBingoCage' and 'list'
❶ 使用 5 个元素(vowels 中的各个字母)创建一个 globe 实例。
❷ 从中取出一个元素,确认它在 vowels 中。
❸ 确认 globe 的元素数量减少到 4 个了。
❹ 创建第二个实例,它有 3 个元素。
❺ 把前两个实例加在一起,创建第 3 个实例。这个实例有 7 个元素。
❻ AddableBingoCage 实例无法与列表相加,抛出 TypeError。那个错误消息是 __add__ 方法返回 NotImplemented 时 Python 解释器输出的。
AddableBingoCage 是可变的,实现 __iadd__ 方法后的行为如示例 13-17 所示。
示例 13-17 可以使用
+=运算符载入现有的AddableBingoCage实例(接续示例 13-16)
>>> globe_orig = globe ➊ >>> len(globe.inspect()) ➋ 4 >>> globe += globe2 ➌ >>> len(globe.inspect()) 7 >>> globe += ['M', 'N'] ➍ >>> len(globe.inspect()) 9 >>> globe is globe_orig ➎ True >>> globe += 1 ➏ Traceback (most recent call last): ... TypeError: right operand in += must be 'AddableBingoCage' or an iterable
❶ 复制一份,供后面检查对象的标识。
❷ 现在 globe 有 4 个元素。
❸ AddableBingoCage 实例可以从同属一类的其他实例那里接受元素。
❹ += 的右操作数也可以是任何可迭代对象。
❺ 在这个示例中,globe 始终指代 globe_orig 对象。
❻ AddableBingoCage 实例不能与非可迭代对象相加,错误消息会指明原因。
注意,与 + 相比,+= 运算符对第二个操作数更宽容。+ 运算符的两个操作数必须是相同类型(这里是 AddableBingoCage),如若不然,结果的类型可能让人摸不着头脑。而 += 的情况更明确,因为就地修改左操作数,所以结果的类型是确定的。
通过观察内置
list类型的工作方式,我确定了要对+和+=的行为做什么限制。my_list + x只能用于把两个列表加到一起,而my_list += x可以使用右边可迭代对象x中的元素扩展左边的列表。list.extend()的行为也是这样的,它的参数可以是任何可迭代对象。
我们明确了 AddableBingoCage 的行为,下面来看实现方式,如示例 13-18 所示。
示例 13-18 bingoaddable.py:
AddableBingoCage扩展BingoCage,支持+和+=
import itertools ➊
from tombola import Tombola
from bingo import BingoCage
class AddableBingoCage(BingoCage): ➋
def __add__(self, other):
if isinstance(other, Tombola): ➌
return AddableBingoCage(self.inspect() + other.inspect()) ➍
else:
return NotImplemented
def __iadd__(self, other):
if isinstance(other, Tombola):
other_iterable = other.inspect() ➎
else:
try:
other_iterable = iter(other)
except TypeError: ➏
self_cls = type(self).__name__
msg = "right operand in += must be {!r} or an iterable"
raise TypeError(msg.format(self_cls))
self.load(other_iterable) ➐
return self ➑
❶ “PEP 8—Style Guide for Python Code”(https://www.python.org/dev/peps/pep-0008/#imports)建议,把导入标准库的语句放在导入自己编写的模块之前。
❷ AddableBingoCage 扩展 BingoCage。
❸ __add__ 方法的第二个操作数只能是 Tombola 实例。
❹ 如果 other 是 Tombola 实例,从中获取元素。
❺ 否则,尝试使用 other 创建迭代器。11
11内置的 iter 函数在下一章讨论。这里,本可以使用 tuple(other),这样做是可以的,但是 .load(...) 方法迭代参数时要构建大量元组,资源消耗大。
❻ 如果尝试失败,抛出异常,并且告知用户该怎么做。如果可能,错误消息应该明确指导用户怎么解决问题。
❼ 如果能执行到这里,把 other_iterable 载入 self。
❽ 重要提醒:增量赋值特殊方法必须返回 self。
通过示例 13-18 中 __add__ 和 __iadd__ 返回结果的方式可以总结出就地运算符的原理。
__add__
调用 AddableBingoCage 构造方法构建一个新实例,作为结果返回。
__iadd__
把修改后的 self 作为结果返回。
最后,示例 13-18 中还有一点要注意:从设计上看,AddableBingoCage 不用定义 __radd__ 方法,因为不需要。如果右操作数是相同类型,那么正向方法 __add__ 会处理,因此,Python 计算 a + b 时,如果 a 是 AddableBingoCage 实例,而 b 不是,那么会返回 NotImplemented,此时或许可以让 b 所属的类接手处理。可是,如果表达式是 b + a,而 b 不是 AddableBingoCage 实例,返回了 NotImplemented,那么 Python 最好放弃,抛出 TypeError,因为无法处理 b。
一般来说,如果中缀运算符的正向方法(如
__mul__)只处理与self属于同一类型的操作数,那就无需实现对应的反向方法(如__rmul__),因为按照定义,反向方法是为了处理类型不同的操作数。
我们对 Python 运算符重载的讨论到此结束。