实现__hash__()的正确和好的方法是什么?

我说的是一个函数,它返回一个哈希码,然后用来将对象插入哈希表,也就是字典。

由于__hash__()返回一个整数,并用于将对象“装箱”到哈希表中,我假设返回的整数的值应该为公共数据均匀分布(以减少冲突)。 获得这些值的良好实践是什么?碰撞是个问题吗? 在我的例子中,我有一个小类,它作为一个容器类,包含一些int,一些浮点和字符串。


当前回答

微软研究院的Paul Larson研究了各种各样的哈希函数。他告诉我

for c in some_string:
    hash = 101 * hash  +  ord(c)

工作令人惊讶的好,各种各样的字符串。我发现类似的多项式技术可以很好地计算不同子字段的散列。

其他回答

实现__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__的文档有更多信息,在某些特定情况下可能很有价值。

@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上测试。

实现哈希(以及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'

这取决于您返回的哈希值的大小。逻辑很简单,如果你需要根据4个32位整型的哈希值返回一个32位整型,你会得到冲突。

我更喜欢位运算。比如,下面的C伪代码:

int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);

这样的系统也适用于浮点数,如果您只是将它们作为位值而不是实际表示浮点值,可能会更好。

至于字符串,我几乎不知道。

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))