增量赋值运算符
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 NotImplementeddef __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 创建迭代器。
❻ 如果尝试失败,抛出异常,并且告知用户该怎么做。如果可能,错
误消息应该明确指导用户怎么解决问题。
❼ 如果能执行到这里,把 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__
),因为按照定义,反向方法是为了处理类型不同
的操作数。