实现__hash__()的正确和好的方法是什么?
我说的是一个函数,它返回一个哈希码,然后用来将对象插入哈希表,也就是字典。
由于__hash__()返回一个整数,并用于将对象“装箱”到哈希表中,我假设返回的整数的值应该为公共数据均匀分布(以减少冲突)。 获得这些值的良好实践是什么?碰撞是个问题吗? 在我的例子中,我有一个小类,它作为一个容器类,包含一些int,一些浮点和字符串。
实现__hash__()的正确和好的方法是什么?
我说的是一个函数,它返回一个哈希码,然后用来将对象插入哈希表,也就是字典。
由于__hash__()返回一个整数,并用于将对象“装箱”到哈希表中,我假设返回的整数的值应该为公共数据均匀分布(以减少冲突)。 获得这些值的良好实践是什么?碰撞是个问题吗? 在我的例子中,我有一个小类,它作为一个容器类,包含一些int,一些浮点和字符串。
当前回答
这取决于您返回的哈希值的大小。逻辑很简单,如果你需要根据4个32位整型的哈希值返回一个32位整型,你会得到冲突。
我更喜欢位运算。比如,下面的C伪代码:
int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);
这样的系统也适用于浮点数,如果您只是将它们作为位值而不是实际表示浮点值,可能会更好。
至于字符串,我几乎不知道。
其他回答
这取决于您返回的哈希值的大小。逻辑很简单,如果你需要根据4个32位整型的哈希值返回一个32位整型,你会得到冲突。
我更喜欢位运算。比如,下面的C伪代码:
int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);
这样的系统也适用于浮点数,如果您只是将它们作为位值而不是实际表示浮点值,可能会更好。
至于字符串,我几乎不知道。
John Millikin提出了一个类似的解决方案:
class A(object):
def __init__(self, a, b, c):
self._a = a
self._b = b
self._c = c
def __eq__(self, othr):
return (isinstance(othr, type(self))
and (self._a, self._b, self._c) ==
(othr._a, othr._b, othr._c))
def __hash__(self):
return hash((self._a, self._b, self._c))
这个解决方案的问题是哈希(A(A, b, c)) ==哈希((A, b, c))。换句话说,哈希值与其关键成员的元组的哈希值发生碰撞。也许在实践中这并不重要?
更新:Python文档现在建议像上面的例子一样使用元组。注意,文档声明
唯一需要的属性是比较相等的对象具有相同的哈希值
请注意,相反的情况并非如此。比较不相等的对象可能具有相同的哈希值。这样的散列碰撞不会导致一个对象在用作dict键或set元素时替换另一个对象,只要对象之间的比较不相等。
过时的/糟糕的解决方案
__hash__的Python文档建议使用类似XOR的东西组合子组件的哈希值,这给了我们这样的结果:
class B(object):
def __init__(self, a, b, c):
self._a = a
self._b = b
self._c = c
def __eq__(self, othr):
if isinstance(othr, type(self)):
return ((self._a, self._b, self._c) ==
(othr._a, othr._b, othr._c))
return NotImplemented
def __hash__(self):
return (hash(self._a) ^ hash(self._b) ^ hash(self._c) ^
hash((self._a, self._b, self._c)))
更新:正如blackknight所指出的,改变a、b和c的顺序可能会导致问题。我添加了一个额外的^ hash((self。_a,自我。_b, self._c))来捕获被散列的值的顺序。如果组合的值不能重新排列(例如,如果它们具有不同的类型,因此_a的值将永远不会被分配给_b或_c,等等),则可以删除最后的^哈希(…)。
@dataclass(frozen=true) (Python 3.7)
这个很棒的新特性,除其他优点外,自动为你定义__hash__和__eq__方法,使其在dicts和set中正常工作:
dataclass_cheat.py
from dataclasses import dataclass, FrozenInstanceError
@dataclass(frozen=True)
class MyClass1:
n: int
s: str
@dataclass(frozen=True)
class MyClass2:
n: int
my_class_1: MyClass1
d = {}
d[MyClass1(n=1, s='a')] = 1
d[MyClass1(n=2, s='a')] = 2
d[MyClass1(n=2, s='b')] = 3
d[MyClass2(n=1, my_class_1=MyClass1(n=1, s='a'))] = 4
d[MyClass2(n=2, my_class_1=MyClass1(n=1, s='a'))] = 5
d[MyClass2(n=2, my_class_1=MyClass1(n=2, s='a'))] = 6
assert d[MyClass1(n=1, s='a')] == 1
assert d[MyClass1(n=2, s='a')] == 2
assert d[MyClass1(n=2, s='b')] == 3
assert d[MyClass2(n=1, my_class_1=MyClass1(n=1, s='a'))] == 4
assert d[MyClass2(n=2, my_class_1=MyClass1(n=1, s='a'))] == 5
assert d[MyClass2(n=2, my_class_1=MyClass1(n=2, s='a'))] == 6
# Due to `frozen=True`
o = MyClass1(n=1, s='a')
try:
o.n = 2
except FrozenInstanceError as e:
pass
else:
raise 'error'
正如我们在这个例子中看到的,哈希值是基于对象的内容计算的,而不仅仅是基于实例的地址。这就是为什么会有这样的事情:
d = {}
d[MyClass1(n=1, s='a')] = 1
assert d[MyClass1(n=1, s='a')] == 1
即使第二个MyClass1(n=1, s='a')是一个与第一个完全不同的实例,具有不同的地址,也可以工作。
frozen=True是强制的,否则类是不可哈希的,否则用户可能会在容器用作键后修改对象而无意中使容器不一致。更多文档:https://docs.python.org/3/library/dataclasses.html
在Python 3.10.7, Ubuntu 22.10上测试。
programiz网站上有一个关于何时以及如何实现__hash__函数的很好的解释:
只是一个截图来提供一个概述: 检索(2019-12-13)
至于该方法的个人实现,上面提到的站点提供了一个与millerdev的答案相匹配的示例。
class Person:
def __init__(self, age, name):
self.age = age
self.name = name
def __eq__(self, other):
return self.age == other.age and self.name == other.name
def __hash__(self):
print('The hash is:')
return hash((self.age, self.name))
person = Person(23, 'Adam')
print(hash(person))
微软研究院的Paul Larson研究了各种各样的哈希函数。他告诉我
for c in some_string:
hash = 101 * hash + ord(c)
工作令人惊讶的好,各种各样的字符串。我发现类似的多项式技术可以很好地计算不同子字段的散列。