什么时候使用hash()调用__eq__?

时间:2017-06-13 20:33:04

标签: python dictionary hash

如上所述here

代码下方

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

2 个答案:

答案 0 :(得分:9)

简短回答:字典查找在搜索存储区时首先执行(便宜)引用相等性检查x is y),并且只有在失败时, (更昂贵的)平等检查(x == y)已完成。

方案

__hash__功能在内部执行不会调用 __eq__。鉴于您构造bobjim,不会调用此类方法。

接下来,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'

答案 1 :(得分:2)

回答标题:

  

何时使用hash()调用__eq__

从不。

另一个问题:

  

为什么在访问jim时会调用__eq__而不是在bob上调用?

那更复杂。要了解您需要知道如何实现字典。假设CPython它将是一个包含hash列,key列和value列的表:

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

它将具有一定的大小,但不足以包含每个可能的hash值,因此它将根据hash计算位置。例如,如果你添加bob它可能有(字符串哈希在某些CPython版本中随机化,因此实际结果会有所不同)hash 7475314405837642385。假设字典的实际大小为2(实际上它会更大,但这会不必要地浪费答案中的空间)它只需要模数,所以它会将它放在7475314405837642385 % 2 == 1中:

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

当你想查找一个键时

  • 始终计算查找的hash
  • 然后它将计算位置。
  • 然后将查询的hash与该位置保存的hash进行比较
  • 如果hash es相等,则会将查找与保存的keyPyObject_RichCompareBool进行比较。那会:
    • 首先检查身份:lookup is key
    • 如果是False,则会检查lookup == key

所以如果你查找bob

  • 哈希:7475314405837642385
  • 职位: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不相等或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,因为它假设所有键都是不可变的!因此,字典会假设它会被保存在另一个位置,或者如果位置(奇迹般地)是相同的,那么它将在包含实际哈希值的步骤中失败。