我经常使用时髦的东西作为词典的键,因此,我想知道什么是正确的方法 - 这通过为我的对象实现好的哈希方法。我知道此处提出的其他问题,例如good way to implement hash,但我想了解默认__hash__
如何适用于自定义对象,以及是否可以依赖它。
我注意到,由于hash({})
引发了错误,因此mutables显然是不可删除的......但奇怪的是,自定义类是可以清除的:
>>> class Object(object): pass
>>> o = Object()
>>> hash(o)
那么,有人知道这个默认哈希函数是如何工作的吗?通过理解这一点,我想知道:
如果我将相同类型的对象放入字典的键中,我可以依赖此默认哈希吗?例如:
key1 = MyObject()
key2 = MyObject()
key3 = MyObject()
{key1: 1, key2: 'blabla', key3: 456}
如果我使用不同类型的对象作为字典中的键,我可以依赖它吗? e.g。
{int: 123, MyObject(10): 'bla', 'plo': 890}
在最后一个案例中,如何确保我的自定义哈希与内置哈希不会发生冲突?例如:
{int: 123, MyObject(10): 'bla', MyObjectWithCustomHash(123): 890}
答案 0 :(得分:24)
您可以信赖的内容:自定义对象具有默认的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中,关系将始终是随机的。实际上,目前的随机化只适用于散列字符串,所以实际上除以16的关系可能会暂时保持,但不要依赖它。
散列冲突通常不重要:在字典查找中查找对象时,它必须具有相同的散列,并且还必须比较相等。碰撞只会影响很大比例的冲突,例如拒绝服务攻击导致最近版本的Python能够随机化哈希计算。
答案 1 :(得分:10)
documentation表示自定义对象依赖id()
作为hash()
实现:
CPython实现细节:这是内存中对象的地址。
如果您将自定义对象与内置类型(如int
)混合在一起,则可能是哈希冲突,但如果它们是均匀分布的话,则完全没问题。除非你真的遇到性能问题,否则不要进行太多调查。
答案 2 :(得分:8)
在Python 3中,以下函数用于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;
}
SIZEOF_VOID_P
对于64位Python为8,对于32位Python为4。
>>> class test: pass
...
>>> a = test()
>>> id(a)
4325845928
>>> hash(a)
-9223372036584410438
您可以看到哈希是使用公式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上执行相同的操作,则会因为不溢出而获得不同的行为)。请参阅https://stackoverflow.com/a/5994397/161801。
答案 3 :(得分:7)
用户定义类的默认哈希就是返回它们的id。这给出了一种通常有用的行为;使用用户定义的类的实例作为字典键将允许在再次提供完全相同的对象以查找值时检索关联的值。 e.g:
>>> 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>
请注意,即使f
和g
的属性值相同,但它们并不相等,在g
中查找d
时找不到值存储在f
下。此外,即使我们更改f.foo
的值,在f
中查找d
仍会找到值:
>>> f.foo = 11
>>> d[f]
10
假设某些任意新类的实例应被视为非等效,除非程序员通过定义__eq__
和__hash__
明确声明两个实例被视为等效的条件。
这非常有用;如果我定义一个Car
类,我可能会认为两辆具有相同属性的汽车代表两辆不同的汽车。如果我有一个字典映射车到注册车主,我不想找到爱丽丝当我查看鲍勃的车,即使爱丽丝和鲍勃碰巧拥有相同的车! OTOH,如果我定义一个代表邮政编码的类,我可能确实想要考虑具有相同代码的两个不同对象是“相同”事物的可互换表示,在这种情况下,如果我有一个字典映射邮政编码到状态,我显然希望能够找到具有代表相同邮政编码的两个不同对象的相同状态。
我将此称为“值类型”和“对象类型”之间的区别。值类型代表一些值,它是我关心的值,而不是每个单独对象的身份。提出相同值的两种不同方式同样好,并且围绕值类型传递的代码的“契约”通常只承诺给出一个具有某种值的对象,而不指定它是哪个特定对象。对于对象类型OTOH,每个单独的实例都有自己的标识,即使它包含与另一个实例完全相同的数据。传递对象类型的代码“契约”通常承诺跟踪确切的单个对象。
那么为什么内置的可变类不使用它们的id作为哈希呢?这是因为它们都是容器,我们通常认为容器大多数类似于值类型,其值由包含的元素决定:
>>> [1, 2, 3] == [1, 2, 3]
True
>>> {f: 10} == {f: 10}
True
但可变容器的值为 transient 。某些给定列表当前的值为[1, 2, 3]
,但可以将其变为值[4, 5, 6]
。如果您可以使用列表作为字典键,那么我们必须对查询是否应该使用列表的(当前)值或其标识做出裁决。无论哪种方式,当通过改变当前用作字典键的对象的值时,我们都会感到非常惊讶。将对象用作字典键仅在对象的值 其标识时,或者当对象的标识与其值无关时才有效。因此,Python选择的答案是声明可变容器不可用。
现在,更具体的细节可以回答您的直接问题:
1)由于CPython中的这个默认哈希(虽然显然只有&lt; 2.6,根据其他答案/注释)映射到对象的内存地址,然后在CPython中没有两个对象使用默认哈希同时存在无论所涉及的类是什么,它们都可能在它们的哈希值上发生冲突(如果它被存储为字典键,它是否存在)。我还希望其他不使用内存地址作为哈希的Python实现仍然应该在使用默认哈希的对象之间进行精细的哈希分配。所以是的,你可以信赖它。
2)只要你没有作为自定义哈希返回一个完全是某个现有对象的哈希的结果,你应该相对没问题。我的理解是Python的基于散列的容器相对容忍次优散列函数,只要它们不是完全退化的。
答案 4 :(得分:2)
>>> class C(object):
... pass
...
>>> c = C()
>>> hash(c) == id(c)
True
参见函数id
答案 5 :(得分:-3)
>>> class C(object):
... pass
...
>>> c = C()
>>> hash(c) == id(c)
False
>>> hash(c) == id(c)/16
True
除以16得到真