如上所述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
?
答案 0 :(得分:9)
简短回答:字典查找在搜索存储区时首先执行(便宜)引用相等性检查(x is y
),并且只有在失败时, (更昂贵的)平等检查(x == y
)已完成。
__hash__
功能在内部执行不会调用 __eq__
。鉴于您构造bob
和jim
,不会调用此类方法。
接下来,将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相等,则会将查找与保存的key
与PyObject_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
,因为它假设所有键都是不可变的!因此,字典会假设它会被保存在另一个位置,或者如果位置(奇迹般地)是相同的,那么它将在包含实际哈希值的步骤中失败。