python中的默认__hash__是什么?
我经常使用时髦的东西作为字典的键,因此,我想知道什么是正确的方式来做到这一点 – 这通过实施好的哈希方法为我的对象。 我知道在这里问的其他问题喜欢好的方法来实现散列 ,但我想了解默认__hash__
如何工作的自定义对象,如果有可能依靠它。
我已经注意到mutables是明确不可能的,因为hash({})
引发了一个错误…但奇怪的是,自定义类是可散列的:
>>> class Object(object): pass >>> o = Object() >>> hash(o)
那么,有没有人知道这个默认的哈希函数是如何工作的? 通过了解这一点,我想知道:
我可以依靠这个默认的哈希,如果我把与字典的键相同types的对象? 例如:
key1 = MyObject() key2 = MyObject() key3 = MyObject() {key1: 1, key2: 'blabla', key3: 456}
如果我在字典中使用不同types的对象作为键,我可以依靠它吗? 例如
{int: 123, MyObject(10): 'bla', 'plo': 890}
而在最后一种情况下,如何确保我的自定义哈希不与内部哈希冲突? 例如:
{int: 123, MyObject(10): 'bla', MyObjectWithCustomHash(123): 890}
你可以依赖的是:自定义对象有一个默认的hash()
,它以某种方式基于对象的身份。 即任何使用默认散列的对象在其整个生命周期中都将具有恒定的散列值,而不同的对象可能有或可能没有不同的散列值。
你不能依赖id()
返回的值和hash()
返回的值之间的任何特定关系。 在Python 2.6及更早版本的标准C实现中,它们是相同的,在Python 2.7-3.2中, hash(x)==id(x)/16
。
编辑:最初我写在版本3.2.3和更高版本或2.7.3或更高版本的哈希值可能是随机的,在Python 3.3中,关系将始终是随机的。 事实上,目前的随机化只适用于散列string,所以实际上16分的关系可能会继续保持,但是不要存在。
哈希碰撞通常不重要:在字典查找中find一个对象,它必须具有相同的哈希值,并且也必须相等。 如果遇到非常高比例的冲突,例如拒绝服务攻击,导致最近版本的Python能够随机散列计算,则冲突才是重要的。
该文档指出自定义对象依赖于id()
作为它们的hash()
实现:
CPython实现细节:这是内存中对象的地址。
如果将自定义对象与像int
这样的内置types混合在一起,它们可能是散列冲突,但如果它们平均分配,则完全没有问题。 除非你真的遇到性能问题,否则不要调查太多。
用户定义的类的默认散列是只返回它们的id。 这给了一个常常有用的行为; 使用用户定义的类的实例作为字典键将允许在再次提供完全相同的对象以查找该值时检索关联的值。 例如:
>>> class Foo(object): def __init__(self, foo): self.foo = foo >>> f = Foo(10) >>> d = {f: 10} >>> d[f] 10
这与用户定义的类的默认相等匹配:
>>> g = Foo(10) >>> f == g False >>> d[g] Traceback (most recent call last): File "<pyshell#9>", line 1, in <module> d[g] KeyError: <__main__.Foo object at 0x0000000002D69390>
请注意,尽pipef
和g
对于它们的属性具有相同的值,但它们并不相同,并且在d
中查找g
并不能findf
存储的值。 此外,即使我们改变f.foo
的值,在d
查找f
仍然可以find值:
>>> f.foo = 11 >>> d[f] 10
假设一些任意新类的实例应该被视为非等价的,除非程序员通过定义__eq__
和__hash__
来明确声明两个实例的条件被视为等价。
而这非常有效。 如果我定义一个Car
类,我可能会考虑两辆具有相同属性的汽车代表两辆不同的汽车。 如果我有一本把汽车映射到注册车主的字典,即使爱丽丝和鲍勃碰巧拥有相同的汽车,我也不想在查找鲍勃的汽车时find爱丽丝! OTOH,如果我定义一个类来表示邮政编码,我可能会想要考虑具有相同代码的两个不同的对象是“相同”事物的可互换表示,在这种情况下,如果我有一个将邮政编码映射到状态,我显然希望能够find代表相同邮政编码的两个不同对象的相同状态。
我将其称为“值types”和“对象types”之间的差异。 价值types代表了一些价值,这是我关心的价值 ,而不是每个单独的对象的身份。 提出相同值的两种不同的方式同样好,并且围绕值types传递的代码的“契约”通常只是给你一个具有某种价值的对象,而不指定它是哪个特定的对象。 对于对象typesOTOH,每个单独的实例都有自己的标识,即使它包含与另一个实例完全相同的数据。 通过对象types传递的代码的“契约”通常会保证跟踪确切的单个对象。
那么为什么内置的可变类使用他们的ID作为他们的哈希? 这是因为它们都是容器 ,我们通常认为容器大部分都是类似于值的types,它们的值由包含的元素决定:
>>> [1, 2, 3] == [1, 2, 3] True >>> {f: 10} == {f: 10} True
但是可变容器的值是暂时的 。 一些给定的列表目前具有[1, 2, 3]
,但是它可以被突变为具有值[4, 5, 6]
。 如果你可以使用列表作为字典键,那么我们就必须判断查询应该使用列表(当前)的值还是它的标识。 无论哪种方式,当一个对象当前用作字典关键字的值被改变时,我们可以(非常)感到惊讶。 使用对象作为字典键时,只有在对象的值是其身份时,或者对象的身份与其值不相关时才能正常工作。 所以Pythonselect的答案是声明不可变的容器。
现在,回答您的直接问题的更具体的细节:
1)由于CPython中的默认散列(虽然显然只有<2.6,根据其他答案/注释)映射到对象的内存地址,然而在CPython中,没有两个同时使用默认散列的对象可能会冲突他们的散列值,而不pipe涉及的类(如果它被存储为一个字典键,它是活的)。 我也希望不使用内存地址作为散列的其他Python实现应该仍然在使用默认散列的对象之间具有良好的散列分布。 所以是的,你可以依靠它。
2)只要你不返回你自定义的哈希结果,这正是一些现有的对象的哈希,你应该是相对好的。 我的理解是,Python的基于散列的容器相对容忍亚次散列函数,只要它们不完全退化。
在Python 3中, object
子类与object
的id()
(来自pyhash.c
)使用以下函数:
Py_hash_t _Py_HashPointer(void *p) { Py_hash_t x; size_t y = (size_t)p; /* bottom 3 or 4 bits are likely to be 0; rotate y by 4 to avoid excessive hash collisions for dicts and sets */ y = (y >> 4) | (y << (8 * SIZEOF_VOID_P - 4)); x = (Py_hash_t)y; if (x == -1) x = -2; return x; }
对于64位Python, SIZEOF_VOID_P
是8,对于32位Python, SIZEOF_VOID_P
是4。
>>> class test: pass ... >>> a = test() >>> id(a) 4325845928 >>> hash(a) -9223372036584410438
您可以看到使用公式(id(a) >> 4) | (id(a) << (8 * SIZEOF_VOID_P - 4))
从id(a)
计算哈希值 (id(a) >> 4) | (id(a) << (8 * SIZEOF_VOID_P - 4))
,其中按C
运算的整数执行按位运算。 例如,对于上面定义的a
:
>>> import numpy >>> y = numpy.array([4325845928], dtype='int64') >>> SIZEOF_VOID_P = 8 >>> (y >> 4) | (y << (8 * SIZEOF_VOID_P - 4)) array([-9223372036584410438])
请注意,我正在使用numpy.array(dtype='int64')
以便按位操作的行为与C中的相同(如果您对Python ints执行相同的操作,则会因为不会溢出而获得不同的行为)。 请参阅https://stackoverflow.com/a/5994397/161801 。
>>> class C(object): ... pass ... >>> c = C() >>> hash(c) == id(c) True
请参阅functionID
>>> class C(object): ... pass ... >>> c = C() >>> hash(c) == id(c) False >>> hash(c) == id(c)/16 True
除以16给出True