2017-06-13 95 views
6

如前所述here什麼時候使用hash()調用__eq__?

下面代碼,

class Person(object): 
    def __init__(self, name, ssn, address): 
     self.name = name 
     self.ssn = ssn 
     self.address = address 
    def __hash__(self): 
     print('in hash') 
     return hash(self.ssn) 
    def __eq__(self, other): 
     print('in eq') 
     return self.ssn == other.ssn 

bob = Person('bob', '1111-222-333', None) 

jim = Person('jim bo', '1111-222-333', 'sf bay area') 


dmv_appointments = {} 
print('calling hash') 
dmv_appointments[bob] = 'tomorrow' 
print('calling hash') 
print(dmv_appointments[jim]) 
print('calling hash again') 
print(dmv_appointments[bob]) 

輸出:

calling hash 
in hash 
calling hash 
in hash 
in eq 
tomorrow 
calling hash again 
in hash 
tomorrow 

問題:

爲什麼__eq__被調用訪問jim而不是bob

+4

我的猜測是,它首先檢查散列,然後_identity_(更快),只有_then_相等。因此,當你查看'bob'時,不必檢查'__eq__'來查看它是否是正確的入口,但是對於'jim'它確實。 –

+0

相關問題:https:// stackoverflow。com/q/40917986/1639625一條評論說明了在C實現中它是如何完成的。 –

+0

@tobias_k:是的,這是一個CPython(Python參考解釋器)實現細節,因爲[使用'PyObject_RichCompareBool'作爲比較C層對象的標準方法](https://docs.python.org/3/c -API/object.html#c.PyObject_RichCompareBool)。使用該函數進行相等性檢查實際上等同於Python級別的「返回x是y或x == y」。 – ShadowRanger

回答

8

簡短的回答:搜索桶時,且僅當出現故障,(更貴)的平等檢查(x == y)做字典查找首先進行(便宜)的參考平等檢查x is y)。

方案

__hash__功能不不叫__eq__內部。給定構造bobjim,則不會調用此類方法。

Next you associate bob'tomorrow'。爲了知道字典中的哪個桶,你必須存儲bob,你的計算散列。現在,一旦你完成了,我們存儲bob(和值在正確的桶)。

接下來我們想要獲得jim。爲了知道在哪個桶中存在jim,我們計算散列。接下來我們開始在桶中搜索。該桶將包含bob。我們首先執行參考檢查jim is bob)但失敗,所以然後我們回退平等檢查。該檢查成功,因此我們返回與bob對應的值:'tomorrow'

當我們想要查找bob時,會發生同樣的情況:我們計算散列,獲取存儲桶。在bob is bob上執行參考檢查,並且該成功。所以我們不需要(可能更昂貴的平等檢查)。我們只需返回值'tomorrow'

參考檢查

參考首先檢查完成的事實可以用下面的(不健康)代碼來證明:

class Person(object): 
    def __init__(self, name, ssn, address): 
     self.name = name 
     self.ssn = ssn 
     self.address = address 
    def __hash__(self): 
     print('in hash') 
     return hash(self.ssn) 
    def __eq__(self, other): print('in eq') return False

在這裏,我們總是False返回平等。因此,即使:

>>> bob == bob 
in eq 
False 
>>> bob is bob 
True 

bob不等於自己(其實這是不好的設計,因爲對於一個字典,它是一個合同,一個對象是等於本身:一個好平等關係反思對稱可遷移)。然而,如果我們聯想bob'tomorrow',我們仍然能夠取得與bob關聯的值:

>>> dmv_appointments = {} 
>>> dmv_appointments[bob] = 'tomorrow' 
in hash 
>>> dmv_appointments[bob] 
in hash 
'tomorrow' 
+0

通常不要求所有對象都等於自己。例如,'float('nan')'和'decimal.Decimal('nan')'不包含這些類型,而Python則包含這些類型。它*是*要求'=='是dict鍵的等價關係,所以使用這些'Person'對象之一作爲dict鍵打破了dict鍵的契約。 – user2357112

+0

@ user2357112:是的,有一些例外,因爲通常將'NaN'視爲「損壞」的結果。來自不同計算的兩個「南」通常是不相等的。但總體上反思性,對稱性和傳遞性是一個良好的平等關係的特性。 –

2

要回答標題:

什麼時候__eq__被使用哈希稱爲()?

從來沒有。


另一個問題:

爲什麼__eq__被調用有關訪問吉姆但不是鮑勃?

這更復雜。要了解你需要知道字典是如何實現的。假設CPython的將是含hash列,一列keyvalue列的表:

hash  | key  | value 
----------------------------------------- 
    - |  - |  - 
----------------------------------------- 
    - |  - |  - 

這將具有一定的規模,但不會大到足以容納每一個可能hash價值,所以它將根據hash計算位置。例如,如果您添加bob它可能有(字符串哈希在某些CPython版本中隨機化,因此實際結果會有所不同)的7475314405837642385。假設詞典中的2的實際尺寸(在現實中,它會更大,但這會不必要地浪費在答題空間),它只是需要模,所以它會放置在7475314405837642385 % 2 == 1

hash  | key  | value 
----------------------------------------- 
    - |  - |  - 
----------------------------------------- 
747...385| bob | 'tomorrow' 

當你要查找的關鍵是

  • 總是計算查找
  • 那麼它將計算位置的hash
  • 然後如果hash ES是相等的,然後它會比較查找和保存keyPyObject_RichCompareBool查找的hash比較保存在該位置
  • hash。這將:
    • 身份第一次檢查:lookup is key
    • 如果是False它會檢查lookup == key

所以,如果你查找bob

  • 哈希:7475314405837642385
  • po sition:7475314405837642385 % 2 - >1
  • 找到一個條目,所以比較hash ES:7475314405837642385 == 7475314405837642385
  • 這是相等,以檢查身份:bob is bob - >True

所以它返回'tomorrow'不相等性檢查。在第二種情況下它會檢查jim

  • 散列:7475314405837642385
  • 位置:7475314405837642385 % 2 - >1
  • 發現了一個條目,因此比較hash ES:7475314405837642385 == 7475314405837642385
  • 這是相等,從而檢查身份:jim is bob - >False
  • 檢查是否相等jim == bob - >True

所以它返回'tomorrow'


這只是實際實現(它缺少一些細節)的近似值。如果hash es不相等或lookup is not key and lookup != key會變得更加複雜,但這些對於瞭解您所質疑的觀察行爲並不重要。

但是,我真的需要說這個:你在做什麼是非常危險的,因爲你的類不是不可變的。你可能會不小心使保存的字典條目不和你在一起:

dmv_appointments = {bob: 1} 
bob.ssn = '1'  # changing ssn changes the hash! 
dmv_appointments[bob] 
--------------------------------------------------------------------------- 
KeyError         Traceback (most recent call last) 
<ipython-input-35-3920ada7bab1> in <module>() 
    15 dmv_appointments = {bob: 1} 
    16 bob.ssn = '1' 
---> 17 dmv_appointments[bob] 

KeyError: <__main__.Person object at 0x000001BD5DDCC470> 

(它仍然在工作的情況下,新hash等於「舊」散,但是這將是相當偶然的)。

這是因爲當你改變你的實例的hash - 字典將不會更新保存的hash,因爲它假定所有的密鑰都是不可變的!所以字典會認爲它會被保存在另一個位置,或者如果這個位置(奇蹟般地)是相同的,那麼它將在包含實際哈希的步驟中失敗。