十三、正確重載運算符
運算符重載的作用:讓用戶定義的對象使用中綴運算符或一元運算符。
Python 施加了一些限制,做好了靈活性、可用性和安全性方面的平衡:
1 不能重載內置類型的運算符
2 不能新建運算符,只能重載現有的
3 某些運算符不能重載—— is , and , or , not (位運算符 & , | , ~ 可以)
一元運算符
-
: __neg__
一元取負算術運算符。
+
: __pos__
一元取正算術運算符。
~
: __inveret__
對整數按位取反, ~x == -(x+1)
abs
: __abs__
取絕對值
一元運算符,只有一個參數 :self ,返回:一個新的同類型對象
def __abs__(self):
return math.sqrt(sum(x * x for x in self))
def __neg__(self):
return Vector(-x for x in self)
def __pos__(self):
return Vector(self)
x 和 +x 何時不相等
雖然每個 +one_third 表達式都會使用 one_third 的值創建一個新 Decimal 實例,但是會使用當前算術運算上下文的精度。精度不同,導致不相等。
In [8]: import decimal
In [9]: ctx = decimal.getcontext() # 獲取當前全局運算符的上下文引用
In [10]: ctx.prec = 40 # 設置算術運算符上下文精度為:40
In [11]: a = decimal.Decimal('1')/decimal.Decimal('3') # 求 1/3
In [12]: a
Out[12]: Decimal('0.3333333333333333333333333333333333333333')
In [13]: a == +a # 此時為 True
Out[13]: True
In [14]: ctx.prec = 28 # 調整精度
In [15]: a == +a # 為 False
Out[15]: False
In [16]: +a
Out[16]: Decimal('0.3333333333333333333333333333')
In [17]: a
Out[17]: Decimal('0.3333333333333333333333333333333333333333')
collections.Counter 會清除計數器中的 負數數量和零數量的值,導致不相等。
In [36]: from collections import Counter
In [37]: c = Counter('abcde')
In [38]: c
Out[38]: Counter({'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})
In [39]: c['a'] = -1
In [40]: c
Out[40]: Counter({'a': -1, 'b': 1, 'c': 1, 'd': 1, 'e': 1})
In [41]: c['b'] = 0
In [42]: c
Out[42]: Counter({'a': -1, 'b': 0, 'c': 1, 'd': 1, 'e': 1})
In [43]: +c
Out[43]: Counter({'c': 1, 'd': 1, 'e': 1})
In [44]: c == +c
Out[44]: False
重載向量加法運算符 +
實現效果:
>>> v1 = Vector([3, 4, 5, 6])
>>> v3 = Vector([1, 2])
>>> v1 + v3
Vector([4.0, 6.0, 5.0, 6.0])
代碼:
def __add__(self, other):
pairs = itertools.zip_longest(self, other, fillvalue=0) # 根據長度長的,填充0,生成器
return Vector(a + b for a, b in pairs) # 生成器,返回 Vector 新實例,不影響 self 或 other
實現一元運算符和中綴運算符的特殊方法一定不能修改操作數。使用這些運算符的表達式期待結果是新對象。只有增量賦值表達式可能會修改第一個操作數(self)。
為了支持涉及不同類型的運算,Python 為中綴運算符特殊方法提供了特殊的分派機制。對表達式 a + b 來說,解釋器會執行以下幾步操作
(1) 如果 a 有 __add__
方法,而且返回值不是 NotImplemented,調用 a.__add__(b)
,然后返回結果。
(2) 如果 a 沒有 __add__
方法,或者調用 __add__
方法返回 NotImplemented,檢查 b有沒有 __radd__
方法,如果有,而且沒有返回 NotImplemented,調用 b.__radd__(a)
,然后返回結果。
(3) 如果 b 沒有 __radd__
方法,或者調用 __radd__
方法返回 NotImplemented,拋出TypeError,並在錯誤消息中指明操作數類型不支持。
__radd__
是 __add__
的“反射”(reflected)版本或“反向”(reversed)版本。我喜歡把它叫作“反向”特殊方法。本書的三位技術審校,Alex、Anna 和 Leo 告訴我,他們喜歡稱之為“右向”(right)特殊方法,因為他們在右操作數上調用。
反射特殊方法,反向特殊方法,右向特殊方法:__radd__
我們要實現 Vector.__radd__
方法。這是一種后備機制,如果左操作數沒有實現 __add__
方法,或者實現了,但是返回 NotImplemented 表明它不知道如何處理右操作數,那么 Python 會調用 __radd__
方法。
NotImplemented
特殊的單例值,如果中綴運算符特殊方法不能處理給定的操作數,那么要把它返回(return)給解釋器。
NotImplementedError
一種異常,抽象類中的占位方法把它拋出(raise),提醒子類必須覆蓋。
實現 __radd__
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): # 實現反向特殊方法,直接委托 __add__。任何可交換的運算符都能這么做。
return self + other
處理數字和向量時,+ 可以交換,但是拼接序列時不行。會拋出沒有太多作用的異常。
如果類型不兼容,就返回 NotImplemented,反向方法也返回 NotImplemented,就拋出標准錯誤消息:
“unsupported operand type(s) for +: Vector and str”
為了遵守鴨子類型精神,我們不能測試 other 操作數的類型,或者它的元素的類型。我們要捕獲異常,然后返回 NotImplemented。
實現:
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
重載標量乘法運算符 *
白鵝類型的實際運用——顯式檢查抽象類型。
def __mul__(self, scalar):
if isinstance(scalar, numbers.Real):
return Vector(n * scalar for n in self)
else:
return NotImplemented
def __rmul__(self, scalar):
return self * scalar
比較運算符
Python 解釋器對眾多比較運算符(==、!=、>、<、>=、<=)的處理與前文類似,不過在兩個方面有重大區別。
1 正向和反向調用使用的是同一系列方法。這方面的規則如表 13-2 所示。例如,對 == 來說,正向和反向調用都是 __eq__
方法,只是把參數對調了;而正向的 __gt__
方法調用的是反向的 __lt__
方法,並把參數對調。
2 對 == 和 != 來說,如果反向調用失敗,Python 會比較對象的 ID,而不拋出 TypeError。
class Vector:
def __eq__(self, other):
return (len(self) == len(other) and all(a == b for a, b in zip(self, other)))
上述方法,列表也可以進行比較。
“Python 之禪”說道:
如果存在多種可能,不要猜測。
對操作數過度寬容可能導致令人驚訝的結果,而程序員討厭驚喜。
從 Python 自身來找線索,我們發現 [1,2] == (1, 2) 的結果是 False。因此,我們要保守一點,做些類型檢查。如果第二個操作數是 Vector 實例(或者 Vector 子類的實例),那么就使用 __eq__
方法的當前邏輯。否則,返回 NotImplemented,讓 Python 處理。
改進后:
def __eq__(self, other):
if isinstance(other, Vector):
return (len(self) == len(other) and all(a == b for a, b in zip(self, other)))
else:
return NotImplemented
>>> va = Vector([1.0, 2.0, 3.0])
>>> vb = Vector(range(1, 4))
>>> va == vb
True
>>> vc = Vector([1, 2])
>>> from vector2d_v3 import Vector2d
>>> v2d = Vector2d(1, 2)
>>> vc == v2d
True
>>> t3 = (1, 2, 3)
>>> va == t3
False
(1) 為了計算 vc == v2d,Python 調用 Vector.__eq__(vc, v2d)。
(2) 經 Vector.__eq__(vc, v2d) 確認,v2d 不是 Vector 實例,因此返回NotImplemented。
(3) Python 得到 NotImplemented 結果,嘗試調用 Vector2d.__eq__(v2d, vc)。
(4) Vector2d.__eq__(v2d, vc) 把兩個操作數都變成元組,然后比較,結果是True
(1) 為了計算 va == t3,Python 調用 Vector.__eq__(va, t3)。
(2) 經 Vector.__eq__(va, t3) 確認,t3 不是 Vector 實例,因此返回NotImplemented。
(3) Python 得到 NotImplemented 結果,嘗試調用 tuple.__eq__(t3, va)。
(4) tuple.__eq__(t3, va) 不知道 Vector 是什么,因此返回 NotImplemented。
(5) 對 == 來說,如果反向調用返回 NotImplemented,Python 會比較對象的 ID,作最后一搏。
增量賦值運算符
如果一個類沒有實現表 13-1 列出的就地運算符,增量賦值運算符只是語法糖:a += b 的作用與 a = a + b 完全一樣。對不可變類型來說,這是預期的行為,而且,如果定義了 __add__
方法的話,不用編寫額外的代碼,+= 就能使用。
結果與預期相符,創建了新的 Vector 實例。
然而,如果實現了就地運算符方法,例如 __iadd__
,計算 a += b 的結果時會調用就地運算符方法。這種運算符的名稱表明,它們會就地修改左操作數,而不會創建新對象作為結果。
不可變類型,一定不能實現就地特殊方法。這是明顯的事實,不過還是值得提出來。
注意,與 + 相比,+= 運算符對第二個操作數更寬容。+ 運算符的兩個操作數必須是相同類型(這里是 AddableBingoCage),如若不然,結果的類型可能讓人摸不着頭腦。而 += 的情況更明確,因為就地修改左操作數,所以結果的類型是確定的。
通過觀察內置 list 類型的工作方式,我確定了要對 + 和 += 的行為做什么限制。 my_list + x 只能用於把兩個列表加到一起,而 my_list += x 可以使用右邊可迭代對象 x 中的元素擴展左邊的列表。list.extend() 的行為也是這樣的,它的參數可以是任何可迭代對象。
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:
raise TypeError(msg.format(self_cls))
self.load(other_iterable)
return self # 重要提醒:增量賦值特殊方法必須返回 self
__add__
調用構造方法構建一個新實例,作為結果返回。
__iadd__
把修改后的 self 作為結果返回。
一般來說,如果中綴運算符的正向方法(如 __mul__
)只處理與 self 屬於同一類型的操作數,那就無需實現對應的反向方法(如 __rmul__
),因為按照定義,反向方法是為了處理類型不同的操作數。
Python 對運算符重載施加的一些限制:禁止重載內置類型的運算符,而且限於重載現有的運算符,不過有幾個例外(is、and、or、not)。
如果操作數的類型不同,我們要檢測出不能處理的操作數。本章使用兩種方式處理這個問題:一種是鴨子類型,直接嘗試執行運算,如果有問題,捕獲 TypeError 異常;另一種是顯式使用 isinstance 測試。
這兩種方式各有優缺點:鴨子類型更靈活,但是顯式檢查更能預知結果。如果選擇使用 isinstance,要小心,不能測試具體類,而要測試 numbers.Real 抽象基類,例如 isinstance(scalar,numbers.Real)。這在靈活性和安全性之間做了很好的折中,因為當前或未來由用戶定義的類型可以聲明為抽象基類的真實子類或虛擬子類,
Python 特別處理 == 和 != 的后備機制:從不拋出錯誤,因為 Python 會比較對象的 ID,作最后一搏。
在 Python 編程中,運算符重載經常使用 isinstance 做測試。一般來說,庫應該利用動態類型(提高靈活性),避免顯式測試類型,而是直接嘗試操作,然后處理異常,這樣只要對象支持所需的操作即可,而不必一定是某種類型。但是,Python 抽象基類允許一種更為嚴格的鴨子類型,Alex Martelli 稱之為“白鵝類型”,編寫重載運算符的代碼時經常能用到。
元組:用到的時候才會使用,而不是定義的時候。