实现__hash__()的正确和好的方法是什么?
我说的是一个函数,它返回一个哈希码,然后用来将对象插入哈希表,也就是字典。
由于__hash__()返回一个整数,并用于将对象“装箱”到哈希表中,我假设返回的整数的值应该为公共数据均匀分布(以减少冲突)。 获得这些值的良好实践是什么?碰撞是个问题吗? 在我的例子中,我有一个小类,它作为一个容器类,包含一些int,一些浮点和字符串。
实现__hash__()的正确和好的方法是什么?
我说的是一个函数,它返回一个哈希码,然后用来将对象插入哈希表,也就是字典。
由于__hash__()返回一个整数,并用于将对象“装箱”到哈希表中,我假设返回的整数的值应该为公共数据均匀分布(以减少冲突)。 获得这些值的良好实践是什么?碰撞是个问题吗? 在我的例子中,我有一个小类,它作为一个容器类,包含一些int,一些浮点和字符串。
当前回答
实现哈希(以及list、dict、tuple)的一个好方法是使用__iter__使对象可迭代,从而使对象具有可预测的项目顺序。我们来修改一下上面的例子:
class A:
def __init__(self, a, b, c):
self._a = a
self._b = b
self._c = c
def __iter__(self):
yield "a", self._a
yield "b", self._b
yield "c", self._c
def __hash__(self):
return hash(tuple(self))
def __eq__(self, other):
return (isinstance(other, type(self))
and tuple(self) == tuple(other))
(这里__eq__不是哈希所必需的,但它很容易实现)。
现在添加一些可变成员,看看它是如何工作的:
a = 2; b = 2.2; c = 'cat'
hash(A(a, b, c)) # -5279839567404192660
dict(A(a, b, c)) # {'a': 2, 'b': 2.2, 'c': 'cat'}
list(A(a, b, c)) # [('a', 2), ('b', 2.2), ('c', 'cat')]
tuple(A(a, b, c)) # (('a', 2), ('b', 2.2), ('c', 'cat'))
只有当你试图在对象模型中放入非哈希成员时,事情才会分崩离析:
hash(A(a, b, [1])) # TypeError: unhashable type: 'list'
其他回答
实现哈希(以及list、dict、tuple)的一个好方法是使用__iter__使对象可迭代,从而使对象具有可预测的项目顺序。我们来修改一下上面的例子:
class A:
def __init__(self, a, b, c):
self._a = a
self._b = b
self._c = c
def __iter__(self):
yield "a", self._a
yield "b", self._b
yield "c", self._c
def __hash__(self):
return hash(tuple(self))
def __eq__(self, other):
return (isinstance(other, type(self))
and tuple(self) == tuple(other))
(这里__eq__不是哈希所必需的,但它很容易实现)。
现在添加一些可变成员,看看它是如何工作的:
a = 2; b = 2.2; c = 'cat'
hash(A(a, b, c)) # -5279839567404192660
dict(A(a, b, c)) # {'a': 2, 'b': 2.2, 'c': 'cat'}
list(A(a, b, c)) # [('a', 2), ('b', 2.2), ('c', 'cat')]
tuple(A(a, b, c)) # (('a', 2), ('b', 2.2), ('c', 'cat'))
只有当你试图在对象模型中放入非哈希成员时,事情才会分崩离析:
hash(A(a, b, [1])) # TypeError: unhashable type: 'list'
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))
@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上测试。
实现__hash__()的一个简单、正确的方法是使用键元组。它不会像专用散列那样快,但如果你需要,那么你可能应该在C中实现该类型。
下面是一个使用键进行哈希和相等的例子:
class A:
def __key(self):
return (self.attr_a, self.attr_b, self.attr_c)
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
if isinstance(other, A):
return self.__key() == other.__key()
return NotImplemented
此外,__hash__的文档有更多信息,在某些特定情况下可能很有价值。
这取决于您返回的哈希值的大小。逻辑很简单,如果你需要根据4个32位整型的哈希值返回一个32位整型,你会得到冲突。
我更喜欢位运算。比如,下面的C伪代码:
int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);
这样的系统也适用于浮点数,如果您只是将它们作为位值而不是实际表示浮点值,可能会更好。
至于字符串,我几乎不知道。