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

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

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


当前回答

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

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

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

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

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

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