物件相等性


如果你定義了類別時沒有定義__eq__()方法,則預設使用==比較兩個實例時,會得到與使用is比較相同的結果。例如:
>>> class Some:
...     pass
...
>>> s1 = Some()
>>> s2 = Some()
>>> s1 == s2
False
>>> s3 = s1
>>> s1 == s3
True
>>>


你可以定義__eq__()方法,來定義使用==運算時的實質比較結果。例如:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __eq__(self, that):
if not isinstance(that, Point):
return False
return self.x == that.x and self.y == that.y

p1 = Point(1, 1)
p2 = Point(1, 1)
print(p1 == p2) # True
print(p1 is p2) # False

如果你試圖將以上定義的Point實例置入集合物件,則會發生錯誤:
pset = {p1} # TypeError: unhashable type: 'Point'

要將實例置入Python的集合物件,該實例必須定義__hash__()方法。在許多場合,例如將物件加入一些群集 (Collection)時,會同時利用__eq__()與__hash__()來判斷是否加入的是(實質上)相同的物件。來看看定義__hash__()時必須遵守的約定(取自java.lang.Object的hashCode() 說明 ):
  • 在同一個應用程式執行期間,對同一物件呼叫 __hash__()方法,必須回傳相同的整數結果。
  • 如果兩個物件使用__eq__()測試 結果為相等, 則這兩個物件呼叫__hash__()時,必須獲得相同的整數結果。
  • 如果兩個物件使用 __eq__()測試結果為不相等, 則這兩個物件呼叫__hash__()時,可以獲得不同的整數結果。

以集合物件為例,會先使用__hash__()得出該將物件放至哪個雜湊桶(Hash buckets)中,如果雜 湊桶有物件,再進一步使用__eq__()確定實質相等性,從而確定集合中不會有重複的物件。以下是定義了__hash__()的Point版本:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __eq__(self, that):
if not isinstance(that, Point):
return False
return self.x == that.x and self.y == that.y

def __hash__(self):
return 41 * (41 + self.x) + self.y

p1 = Point(1, 1)
p2 = Point(1, 1)
pset = {p1}
print(p2 in pset) # True

一個重要的觀念是,定義__eq__()與__hash__()時,最好 別使用狀態會改變的資料成員。你可能會想,以這個例子來說,點會移動,如果移動了就不是相同的點了,不是嗎?若x、y是個允許會變動的成 員,那麼就會發生這個情況:
p1 = Point(1, 1)
pset = {p1}
print(p1 in pset)    # True
p1.x = 2
print(p1 in pset)    # False

明 明是記憶體中同一個物件,但置入集合後,最後跟我說不包括p1?這是因為,你改變了x,算出來的__hash__()也就改變了,使用in嘗試比對時,會看看新算出來的雜湊桶中是不是有物件,而根本不是置入p1的雜湊桶中尋找,結果就是False了。

一個限制修改不可變動成員的方式,是定義__setattr__()方法。例如:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __setattr__(self, name, value):
if not name in self.__dict__:
self.__dict__[name] = value
elif name == 'x' or name == 'y':
raise TypeError('Point(x, y) is immutable')

def __eq__(self, that):
if not isinstance(that, Point):
return False
return self.x == that.x and self.y == that.y

def __hash__(self):
return 41 * (41 + self.x) + self.y

p1 = Point(1, 1)
p2 = Point(1, 1)
pset = {p1}
print(p1 in pset)
p1.x = 2 # TypeError: Point(x, y) is immutable

再來看看在實作__eq__()時要遵守的約定(取自java.lang.Object的 equals() 說明 ):
  • 反身性 (Reflexive):x == x的結果要是True。
  • 對稱性 (Symmetric):x == y與y == x的結果必須相同。
  • 傳遞性 (Transitive):x == y、y == z的結果都是True,則x == z的結果也必須是True。
  • 一 致性(Consistent):同一個執行期間,對x == y的多次呼叫,結果必須相同。
  • 對 任何非None的x,x == None必須傳回False。

目前定義的Point,其__eq__()方法滿足以上幾個約定(你可以自行寫程式測試)。現在考慮繼承的情況,你要定義3D的點:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __eq__(self, that):
if not isinstance(that, Point):
return False
return self.x == that.x and self.y == that.y


class Point3D(Point):
def __init__(self, x, y, z):
super(Point3D, self).__init__(x, y)
self.z = z

def __eq__(self, that):
if not isinstance(that, Point3D):
return False
return super(Point3D, self).__eq__(that) and self.z == that.z


p1 = Point(1, 1)
p2 = Point3D(1, 1, 1)

print(p1 == p2)

在繼承的情況下,若==兩旁運算元有一個是子類別實例,則會使用子類別的__eq__()版本進行比對。在上面的定義之下,直接將2D與3D的點視作不同的類型,這避免了2D點與3D點(父、子類別)進行比較時,無法符合對稱性、傳遞性合約的問題。