在python中,如果我有一个包含许多元素的元组,它的哈希是根据其元素'id
s还是其元素的内容计算的?
在这个例子中,
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
似乎b
和c
的内容用于哈希计算。
我该如何理解这些例子?
都不是。它是根据这些元素的散列计算的,而不是它们的“内容”(值/属性)。
看看python's documentation glossary中的这一段。
是否某些东西是可以清洗的,以及它是如何散列的,取决于它的.__hash__()
方法的实现。 Python本身并不知道对象的可变性。
在你的第一个例子中,tuple
恰好在其元素的基础上散列自己,而list
根本没有散列 - .__hash__()
方法没有为它实现(并且有充分的理由)。这就是为什么tuple
里面有一个list
物体是不可穿的。
现在,考虑到这一点,让我们来看看python data model documentation,以及它对该主题的看法:
用户定义的类默认具有
__eq__()
和__hash__()
方法;与它们相比,所有对象都比较不等(除了它们自己)和x.__hash__()
返回一个合适的值,使得x == y
暗示x is y
和hash(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,其中包含每个元素的哈希值。是否使用这些元素的内容取决于它们的特定散列函数。
哈希的核心契约是等对象具有相等的哈希值。特别是,散列并不直接关注可变性或突变;它只关心影响平等比较的突变。
你的第一个元组是不可用的,因为改变嵌套列表会改变元组在相等比较中的行为方式。
在第二个示例中改变a0
不会影响元组的哈希,因为它不会影响相等比较。 a0
仍然只等于它自己,它的哈希值没有变化。
你的第三个例子中的tb
和tc
具有相等的哈希值,因为它们是相等的元组,无论它们的元素是否是相同的对象。
这一切都意味着元组不能(直接)使用id
进行哈希。如果他们这样做,具有不同但相同元素的相等元组可能会以不同的方式散列,违反哈希合约。如果没有特殊的外壳元素类型,元组可以用来计算它们自己的哈希的唯一东西是它们的元素的哈希,所以元组将它们的哈希基于它们的元素的哈希。
问题的答案“元组的哈希值是根据身份还是价值来计算的?”是:都不是。
正确的答案是元组的哈希值是从元素的哈希值计算出来的。如何计算这些哈希值(或多或少)无关紧要。
证明这一点的一个简单方法是看看将列表放入元组时会发生什么:
>>> 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 = 20
对a0
的哈希值没有影响:
a0 = A()
print(hash(a0)) # -9223363274645980307
a0.x = 20
print(hash(a0)) # -9223363274645980307
原因是python为你实现了一个默认的哈希函数。来自the docs:
用户定义的类默认具有
__eq__()
和__hash__()
方法;与它们相比,所有对象都比较不等(除了它们自己)和x.__hash__()
返回一个合适的值,使得x == y
暗示x is y
和hash(x) == hash(y)
。
默认的哈希函数忽略对象的属性,并根据对象的id计算哈希值。无论你对a0
做出什么改变,它的哈希都会保持不变。 (虽然可以通过实现自定义A
方法为__hash__
类的实例定义自定义哈希函数。)
附录:列表不可清除的原因是因为它们是可变的。来自the docs:
如果一个类定义了可变对象并实现了
__eq__()
方法,那么它不应该实现__hash__()
,因为hashable集合的实现要求键的哈希值是不可变的(如果对象的哈希值改变,它将在错误的哈希桶中)。
列表属于此类别。
tuple
的散列基于内容,而不是基于元组的_id_s。并且以递归方式计算哈希:如果一个元素不可哈希(如list
元素),则元组本身不可清除。
这是完全正常的,如果a
和b
是元组和a == b
,那么hash(a) == hash(b)
(如果哈希可以计算当然),即使a is not b
。
(相反hash(a) == hash(b)
并不意味着a == b
)
is
传达的信息通常不是很有用,因为例如python对象实习。