如果对象的__hash__发生变化会怎样?
在Python中,__hash__
这个值对于一个对象来说,在它的整个生命周期内应该是保持不变的。不过,我很好奇,如果这个值不一样了,会发生什么呢?会造成什么样的麻烦呢?
class BadIdea(object):
def __hash__(self):
return random.randint(0, 10000)
我知道如果__contains__
和__getitem__
的行为变得奇怪,那么字典和集合也会因此表现得很奇怪。这样可能会导致字典或集合中出现“孤立”的值。
还有其他什么情况会发生吗?会不会导致解释器崩溃,或者破坏内部结构呢?
2 个回答
在Github上有一篇很棒的文章,讲的是关于哈希的内容:当你搞乱哈希时会发生什么。首先,你需要知道Python对哈希的期望(摘自文章):
一个对象的哈希值在它的生命周期内是不会改变的(换句话说,一个可哈希的对象应该是不可变的)。
a == b
意味着hash(a) == hash(b)
(注意,反过来不一定成立,特别是在哈希碰撞的情况下)。
下面是一个代码示例,展示了变体哈希的问题,虽然用的是稍微不同的类,但想法是一样的:
>>> class Bad(object):
... def __init__(self, arg):
... self.arg = arg
... def __hash__(self):
... return hash(self.arg)
...
>>> Bad(1)
<__main__.Bad object at ...>
>>> hash(Bad(1))
1
>>> a = Bad(1)
>>> b = {a:1}
>>> a.arg = 2
>>> hash(a)
2
>>> b[a]
Traceback (most recent call last):
...
KeyError: <__main__.Bad object at ...>
在这里,我们通过改变用于计算哈希的参数,隐式地改变了a的哈希值。结果,这个对象在字典中找不到了,因为字典是通过哈希值来查找对象的。
要注意的是,Python并不会阻止我这样做。如果我想的话,可以让
__setattr__
抛出AttributeError
,但即便如此,我仍然可以通过修改对象的__dict__
来强行改变它。这就是我们所说的Python是一个“同意成年人”的语言。
这不会导致Python崩溃,但会在字典、集合以及所有基于对象哈希的东西上出现意想不到的行为。
你的主要问题确实出在字典和集合上。如果你把一个对象放进字典或集合里,而这个对象的哈希值发生了变化,那么当你想要取出这个对象时,你会在字典或集合的底层数组中寻找一个不同的位置,因此找不到这个对象。这就是为什么字典的键应该始终是不可变的原因。
这里有个简单的例子:假设我们把o
放进一个字典,o
的初始哈希值是3。我们可以这样做(稍微简化一下,但能说明问题):
Hash table: 0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | | | | o | | | | | +---+---+---+---+---+---+---+---+ ^ we put o here, since it hashed to 3
现在假设o
的哈希值变成了6
。如果我们想从字典中取出o
,我们会去看位置6
,但那里什么都没有!这会导致在查询数据结构时出现错误的结果。实际上,上面数组的每个元素在字典的情况下都可能有一个“值”与之关联,并且在同一个位置可能有多个元素(例如,哈希冲突)。此外,我们通常在决定把元素放在哪里时,会将哈希值对数组的大小取模。尽管有这些细节,但上面的例子仍然准确地传达了当对象的哈希值变化时可能出现的问题。
这会导致解释器崩溃,或者破坏内部结构吗?
不会,这种情况不会发生。当我们说对象的哈希值变化是“危险”的时候,我们是指这会基本上破坏哈希的目的,使得代码变得难以理解,甚至无法推理。我们并不是说这会导致崩溃。