python如何计算元组的哈希值

问题描述 投票:29回答:4

在python中,如果我有一个包含许多元素的元组,它的哈希是根据其元素'ids还是其元素的内容计算的?

在这个例子中,

a = (1, [1,2])
hash(a)

它错误地说列表是不可用的。所以我猜它不是由id计算的,或者可能是检查元素是否可变。

现在看这个例子

class A: pass
a0 = A()
ta = (1, a0)
hash(ta)  # -1122968024
a0.x = 20
hash(ta)  # -1122968024

在这里,事实证明ta的散列不随其元素的修改而改变,即a0。那么也许a0的id用于哈希计算? a0被认为是不可改变的吗? python如何知道类型是否可变?

现在考虑这个案例

b = (1, 2)
id(b)  # 3980742764
c = (1, 2)
id(c)  # 3980732588
tb = (1, b)
tc = (1, c) 
hash(tb)  # -1383040070
hash(tc)  # -1383040070

似乎bc的内容用于哈希计算。

我该如何理解这些例子?

python hash tuples
4个回答
24
投票

都不是。它是根据这些元素的散列计算的,而不是它们的“内容”(值/属性)。

看看python's documentation glossary中的这一段。

是否某些东西是可以清洗的,以及它是如何散列的,取决于它的.__hash__()方法的实现。 Python本身并不知道对象的可变性。

在你的第一个例子中,tuple恰好在其元素的基础上散列自己,而list根本没有散列 - .__hash__()方法没有为它实现(并且有充分的理由)。这就是为什么tuple里面有一个list物体是不可穿的。

现在,考虑到这一点,让我们来看看python data model documentation,以及它对该主题的看法:

用户定义的类默认具有__eq__()__hash__()方法;与它们相比,所有对象都比较不等(除了它们自己)和x.__hash__()返回一个合适的值,使得x == y暗示x is yhash(x) == hash(y)

这就是为什么你不必为你的类定义.__hash__() - 在这种情况下python为你做了。默认实现不会考虑实例字段。这就是为什么您可以在不更改其哈希值的情况下更改对象内部的值。

在这方面你是对的 - 自定义类的哈希函数的默认(CPython)实现依赖于对象的id(),而不依赖于它内部的值。它是一个实现细节,但它在Python版本之间有所不同。在更新的Python版本中,hash()id()之间的关系涉及一些随机化。


但是它实际上如何散列呢?

虽然细节非常复杂并且可能涉及一些高级数学,但是元组对象的散列函数的实现是用C语言编写的,可以看作here(参见static Py_hash_t tuplehash(PyTupleObject *v)

计算涉及使用每个元组元素的哈希对常量进行异或运算。负责元素散列的行是这样的:

y = PyObject_Hash(*p++);

所以,回答你原来的问题:它带有一堆XOR hokus-pocus,其中包含每个元素的哈希值。是否使用这些元素的内容取决于它们的特定散列函数。


8
投票

哈希的核心契约是等对象具有相等的哈希值。特别是,散列并不直接关注可变性或突变;它只关心影响平等比较的突变。


你的第一个元组是不可用的,因为改变嵌套列表会改变元组在相等比较中的行为方式。

在第二个示例中改变a0不会影响元组的哈希,因为它不会影响相等比较。 a0仍然只等于它自己,它的哈希值没有变化。

你的第三个例子中的tbtc具有相等的哈希值,因为它们是相等的元组,无论它们的元素是否是相同的对象。


这一切都意味着元组不能(直接)使用id进行哈希。如果他们这样做,具有不同但相同元素的相等元组可能会以不同的方式散列,违反哈希合约。如果没有特殊的外壳元素类型,元组可以用来计算它们自己的哈希的唯一东西是它们的元素的哈希,所以元组将它们的哈希基于它们的元素的哈希。


3
投票

问题的答案“元组的哈希值是根据身份还是价值来计算的?”是:都不是。

正确的答案是元组的哈希值是从元素的哈希值计算出来的。如何计算这些哈希值(或多或少)无关紧要。

证明这一点的一个简单方法是看看将列表放入元组时会发生什么:

>>> hash( (1, 2) )
3713081631934410656
>>> hash( (1, []) )
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

由于列表不可清除,因此包含列表的元组也不可清除。


让我们仔细看看你带来的这个例子:

class A: pass
a0 = A()
ta = (1, a0)
hash(ta)  # -1122968024
a0.x = 20
hash(ta)  # -1122968024

为什么不设置a0.x = 20会影响元组的哈希?好吧,如果我们修改此代码以输出a0的哈希值,您将看到设置a0.x = 20a0的哈希值没有影响:

a0 = A()
print(hash(a0))  # -9223363274645980307
a0.x = 20
print(hash(a0))  # -9223363274645980307

原因是python为你实现了一个默认的哈希函数。来自the docs

用户定义的类默认具有__eq__()__hash__()方法;与它们相比,所有对象都比较不等(除了它们自己)和x.__hash__()返回一个合适的值,使得x == y暗示x is yhash(x) == hash(y)

默认的哈希函数忽略对象的属性,并根据对象的id计算哈希值。无论你对a0做出什么改变,它的哈希都会保持不变。 (虽然可以通过实现自定义A方法为__hash__类的实例定义自定义哈希函数。)


附录:列表不可清除的原因是因为它们是可变的。来自the docs

如果一个类定义了可变对象并实现了__eq__()方法,那么它不应该实现__hash__(),因为hashable集合的实现要求键的哈希值是不可变的(如果对象的哈希值改变,它将在错误的哈希桶中)。

列表属于此类别。


2
投票

tuple的散列基于内容,而不是基于元组的_id_s。并且以递归方式计算哈希:如果一个元素不可哈希(如list元素),则元组本身不可清除。

这是完全正常的,如果ab是元组和a == b,那么hash(a) == hash(b)(如果哈希可以计算当然),即使a is not b

(相反hash(a) == hash(b)并不意味着a == b

is传达的信息通常不是很有用,因为例如python对象实习。

© www.soinside.com 2019 - 2024. All rights reserved.