在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
的内容用于哈希计算。
我应该如何理解这些例子?
答案 0 :(得分:24)
都不是。它是根据这些元素的散列计算的,而不是它们的“内容”(值/属性)。
在python's documentation glossary中查看此段落。
是否可以哈希,如何哈希值,取决于其.__hash__()
方法的实现。 Python本身并不知道对象的可变性。
在您的第一个示例中,tuple
碰巧根据其元素散列自身,而list
根本没有散列 - .__hash__()
方法未实现因为它(并有充分理由)。这就是为什么tuple
内部带有list
对象的__eq__()
不可用的。
现在,考虑到这一点,让我们看看python data model documentation,以及它对该主题的看法:
默认情况下,用户定义的类具有
__hash__()
和x.__hash__()
方法;与它们相比,所有对象都比较不相等(除了自己),x == y
返回一个适当的值,x is y
同时暗示hash(x) == hash(y)
和.__hash__()
。
这就是为什么你不必为你的类定义hash()
- 在这种情况下python为你做了。默认实现不会考虑实例字段。这就是为什么你可以在不改变其哈希值的情况下改变对象内部的值。
在这方面你是对的 - 自定义类的哈希函数的默认( CPython的)实现依赖于对象的id()
,而不是内部的值。它。它是一个实现细节,但它在Python版本之间有所不同。在更新的Python版本中,id()
和static Py_hash_t tuplehash(PyTupleObject *v)
之间的关系涉及一些随机化。
虽然细节非常复杂并且可能涉及一些高级数学,但是元组对象的散列函数的实现是用C语言编写的,可以看作here(参见y = PyObject_Hash(*p++);
。
计算涉及使用每个元组元素的哈希对常量进行异或运算。负责元素散列的行是这样的:
class User {
const User(this.id,this.name);
final String name;
final int id;
}
所以,回答你原来的问题:它有一堆XOR hokus-pocus,每个元素的哈希。是否使用这些元素的内容取决于它们的特定散列函数。
答案 1 :(得分:8)
哈希的核心契约是相等的对象具有相等的哈希值。特别是,散列并不直接关注可变性或突变;它只关心影响平等比较的突变。
你的第一个元组是不可取的,因为改变嵌套列表会改变元组在相等比较中的行为方式。
在第二个示例中变异a0
不会影响元组的散列,因为它不会影响相等比较。 a0
仍然只与自身相等,其哈希值不变。
tb
和tc
具有相等的哈希值,因为它们是相等的元组,无论它们的元素是否是相同的对象。
这意味着元组不能(直接)使用id
进行散列。如果他们这样做,具有不同但相同元素的相等元组可能会以不同的方式散列,违反哈希合约。如果没有特殊的外壳元素类型,元组可以用来计算自己的哈希的唯一东西就是它们的元素'哈希,所以元组将他们的哈希基于他们的元素'散列。
答案 2 :(得分: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 = 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
做出哪些更改,其哈希值始终保持不变。 (虽然可以通过实现自定义__hash__
方法为A
类的实例定义自定义哈希函数。)
附录:列表不可清除的原因是因为它们是可变的。来自the docs:
如果一个类定义了可变对象并实现了
__eq__()
方法, 自从执行以来,它不应该实现__hash__()
hashable集合要求键的哈希值是不可变的(如果 对象的哈希值发生变化,它将在错误的哈希桶中。)
列表属于此类别。
答案 3 :(得分:2)
tuple
的哈希基于内容,而不是基于元组的_id_s。散列是以递归方式计算的:如果一个元素不可散列(如list
元素),则元组本身不可散列。
如果a
和b
是元组和a == b
,那么hash(a) == hash(b)
(如果当然可以计算哈希值),那就完全正常了,即使a is not b
。
(相反hash(a) == hash(b)
并不代表a == b
)
is
传达的信息通常不是很有用,因为例如python对象实习。