我试图理解引擎盖下的python哈希函数。我创建了一个自定义类,其中所有实例都返回相同的哈希值。
class C(object):
def __hash__(self):
return 42
我只是假设上面的类中只有一个实例可以随时出现在一个集合中,但实际上一个集合可以有多个具有相同散列的元素。
c, d = C(), C()
x = {c: 'c', d: 'd'}
print x
# {<__main__.C object at 0x83e98cc>:'c', <__main__.C object at 0x83e98ec>:'d'}
# note that the dict has 2 elements
我进行了一些实验,发现如果我覆盖__eq__
方法,使得该类的所有实例比较相等,那么该集只允许一个实例。
class D(C):
def __eq__(self, other):
return hash(self) == hash(other)
p, q = D(), D()
y = {p:'p', q:'q'}
print y
# {<__main__.D object at 0x8817acc>]: 'q'}
# note that the dict has only 1 element
所以我很想知道dict怎么能有多个具有相同哈希的元素。谢谢!
注意:编辑问题以给出dict(而不是set)的例子,因为答案中的所有讨论都是关于dicts的。但这同样适用于集合;集合也可以有多个具有相同散列值的元素。
答案 0 :(得分:91)
以下是我能够整理的所有关于Python dicts的内容(可能比任何人都想知道的更多;但答案是全面的)。向Duncan发出警告,指出Python决定使用插槽并引导我走下这个兔子洞。
O(1)
查找)。 下图是python哈希表的逻辑表示。在下图中,左侧的0,1,...,i,...是散列表中插槽的索引(它们仅用于说明目的,不与其一起存储)桌子显然!)。
# Logical model of Python Hash table
-+-----------------+
0| <hash|key|value>|
-+-----------------+
1| ... |
-+-----------------+
.| ... |
-+-----------------+
i| ... |
-+-----------------+
.| ... |
-+-----------------+
n| ... |
-+-----------------+
初始化新的dict时,它以8个插槽开头。 (见dictobject.h:49)
i
开始。 CPython使用初始i = hash(key) & mask
。 mask = PyDictMINSIZE - 1
,但这并不重要。请注意,检查的初始插槽i取决于密钥的哈希。<hash|key|value>
)。但如果那个插槽被占用怎么办?很可能是因为另一个条目具有相同的哈希值(哈希冲突!)==
比较而不是is
比较)插槽中的条目与要插入的当前条目的键(dictobject.c:337,344-345)相对应。如果两者匹配,则认为该条目已存在,放弃并继续前进到要插入的下一个条目。如果散列或密钥不匹配,则启动探测。 ==
)。总而言之,如果有两个键a
和b
以及hash(a)==hash(b)
,但是a!=b
,则两者都可以在Python词典中和谐地存在。但是,如果hash(a)==hash(b)
和 a==b
,那么它们不能同时存在于同一个字典中。
因为我们必须在每次哈希冲突之后进行探测,所以太多哈希冲突的一个副作用是查找和插入将变得非常慢(正如Duncan在comments中指出的那样)。
我想我的问题的简短回答是,“因为它是如何在源代码中实现的;”
虽然这很有用(对于极客点?),但我不确定如何在现实生活中使用它。因为除非你试图明确地破坏某些东西,为什么两个不相等的对象具有相同的哈希?
答案 1 :(得分:40)
有关Python哈希工作方式的详细说明,请参阅我对Why is early return slower than else?
的回答基本上它使用哈希来选择表中的一个槽。如果插槽中有值且散列匹配,则会比较这些项以查看它们是否相等。
如果哈希不匹配或项目不相等,则尝试另一个槽。有一个公式来选择这个(我在引用的答案中描述),它逐渐拉入哈希值的未使用部分;但是一旦它全部使用它们,它最终将通过哈希表中的所有插槽。这保证了我们最终找到匹配的项目或空槽。当搜索找到一个空槽时,它会插入值或放弃(取决于我们是否添加或获取值)。
需要注意的重要事项是没有列表或存储桶:只有一个具有特定插槽数的散列表,每个散列用于生成一系列候选插槽。
答案 2 :(得分:19)
编辑:下面的答案是处理哈希冲突的可能方法之一,然而不 Python如何做到这一点。下面引用的Python的wiki也是不正确的。 @Duncan给出的最佳来源是实现本身:http://svn.python.org/projects/python/trunk/Objects/dictobject.c我为混淆道歉。
它在散列中存储元素的列表(或存储桶),然后遍历该列表,直到找到该列表中的实际键。一张图片说了千言万语:
您在此处看到John Smith
和Sandra Dee
都哈希到152
。 Bucket 152
包含两者。在查找Sandra Dee
时,它首先在存储桶152
中找到该列表,然后循环遍历该列表,直到找到Sandra Dee
并返回521-6955
。
以下是错误的,它仅适用于上下文:在Python's wiki上,您可以找到(伪?)代码,以便Python执行查找。
实际上有几种可能解决这个问题的方法,请查看维基百科文章,以获得一个很好的概述:http://en.wikipedia.org/wiki/Hash_table#Collision_resolution
答案 3 :(得分:4)
哈希表,一般都要允许哈希冲突!你会发现不幸的事情,最终会有两件事情发生在同一件事上。在下面,在具有相同散列键的项列表中有一组对象。通常,该列表中只有一件事,但在这种情况下,它会将它们堆叠在同一个列表中。它知道它们不同的唯一方法是通过equals运算符。
当发生这种情况时,您的性能会随着时间的推移而降低,这就是您希望散列函数尽可能“随机”的原因。
答案 4 :(得分:1)
在线程中,当我们将字典作为键放入字典时,我没有看到python对用户定义类的实例的确切作用。让我们阅读一些文档:它声明只有可以删除的对象可以用作键。 Hashable是所有不可变的内置类和所有用户定义的类。
用户定义的类具有__cmp __()和 __hash __()方法默认情况下;与他们,所有对象 比较不平等(除了自己)和 x .__ hash __()返回从id(x)派生的结果。
因此,如果你的班级中经常有__hash__,但没有提供任何__cmp__或__eq__方法,那么你的所有实例对于字典都是不相等的。 另一方面,如果您提供任何__cmp__或__eq__方法,但不提供__hash__,则您的实例在字典方面仍然不相等。
class A(object):
def __hash__(self):
return 42
class B(object):
def __eq__(self, other):
return True
class C(A, B):
pass
dict_a = {A(): 1, A(): 2, A(): 3}
dict_b = {B(): 1, B(): 2, B(): 3}
dict_c = {C(): 1, C(): 2, C(): 3}
print(dict_a)
print(dict_b)
print(dict_c)
输出
{<__main__.A object at 0x7f9672f04850>: 1, <__main__.A object at 0x7f9672f04910>: 3, <__main__.A object at 0x7f9672f048d0>: 2}
{<__main__.B object at 0x7f9672f04990>: 2, <__main__.B object at 0x7f9672f04950>: 1, <__main__.B object at 0x7f9672f049d0>: 3}
{<__main__.C object at 0x7f9672f04a10>: 3}