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

ove*_*nge 6 python hash dictionary

这里提到的,

下面的代码,

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])
Run Code Online (Sandbox Code Playgroud)

输出:

calling hash
in hash
calling hash
in hash
in eq
tomorrow
calling hash again
in hash
tomorrow
Run Code Online (Sandbox Code Playgroud)

题:

为什么__eq__要求访问jim而不是访问bob

Wil*_*sem 9

简短回答:字典查找首先x is y在搜索存储桶时执行(廉价)引用相等性检查(),并且只有在失败时才会执行(更昂贵的)等式检查(x == y).

脚本

__hash__函数不会 __eq__内部调用.鉴于你构造,bobjim没有调用这样的方法.

接下来你联系bob'tomorrow'.为了知道你必须存储的字典的哪个桶bob,你计算哈希值.现在,一旦你完成了我们存储bob(并将值存储在正确的存储桶中).

接下来我们想获得jim.为了知道哪个桶jim驻留,我们计算哈希值.接下来我们开始在桶中搜索.桶将包含bob.我们首先执行引用check(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
Run Code Online (Sandbox Code Playgroud)

在这里,我们总是回归False平等.甚至:

>>> bob == bob
in eq
False
>>> bob is bob
True
Run Code Online (Sandbox Code Playgroud)

bob不等于它本身(这实际上并不是好的设计,因为对于字典而言,它是一个契约,它是一个对象与自身相等:良好的平等关系反身的,对称的和可传递的).然而,如果我们联想bob'tomorrow',我们依然能够取得与关联的值bob:

>>> dmv_appointments = {}
>>> dmv_appointments[bob] = 'tomorrow'
in hash
>>> dmv_appointments[bob]
in hash
'tomorrow'
Run Code Online (Sandbox Code Playgroud)


MSe*_*ert 8

回答一下标题:

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

绝不。


另一个问题:

为什么__eq__访问 jim 时会被调用,但访问 bob 时不会被调用?

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

hash     | key       | value
-----------------------------------------
    -    |      -    |        -
-----------------------------------------
    -    |      -    |        -
Run Code Online (Sandbox Code Playgroud)

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

hash     | key       | value
-----------------------------------------
    -    |      -    |        -
-----------------------------------------
747...385|    bob    |    'tomorrow'
Run Code Online (Sandbox Code Playgroud)

当你想查找某个键时

  • 总是计算hash查找的值
  • 然后它会计算位置。
  • 然后它将hash查找的值与hash该位置保存的值进行比较
  • 如果hashes 相等,那么它将比较查找和保存的key内容PyObject_RichCompareBool。这将:
    • 首先检查身份:lookup is key
    • 如果是的话,False它将检查lookup == key

所以如果你查找bob

  • 哈希:7475314405837642385
  • 位置:7475314405837642385 % 2->1
  • 找到一个条目,所以比较hashes:7475314405837642385 == 7475314405837642385
  • 这是相等的,所以检查身份:bob is bob->True

所以它返回时'tomorrow'没有进行相等性检查。在第二种情况下,它检查jim

  • 哈希:7475314405837642385
  • 位置:7475314405837642385 % 2->1
  • 找到一个条目,所以比较hashes:7475314405837642385 == 7475314405837642385
  • 这是相等的,所以检查身份:jim is bob->False
  • 检查是否相等jim == bob->True

所以它返回了'tomorrow'


这只是实际实现的近似值(缺少一些细节)。如果hashes 不相等或者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>
Run Code Online (Sandbox Code Playgroud)

(如果新的hash等于“旧”哈希,它仍然可以工作,但这将是相当偶然的)。

这是因为当您更改hash实例的时 - 字典不会更新保存的hash,因为它假设所有键都是不可变的!因此,字典要么假设它将保存在另一个位置,要么如果该位置(奇迹般地)相同,那么它会在包含实际哈希值的步骤中失败。