增量賦值運算符
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__
),因為按照定義,反向方法是為了處理類型不同
的操作數。