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


150

什么是正确的好方法__hash__()

我说的是一个返回哈希码的函数,然后该哈希码用于将对象插入哈希表(又名字典)。

__hash__()返回一个整数并用于将对象“绑定”到哈希表中时,我假设返回的整数的值应均匀分配给公共数据(以最大程度地减少冲突)。获得这样的价值观是什么好习惯?碰撞是个问题吗?就我而言,我有一个小类,它充当一个容器类,其中包含一些整数,一些浮点数和一个字符串。

Answers:


185

一种简单而正确的实现方法__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__还包含更多信息,这些信息在某些特定情况下可能会很有价值。


1
除了因分解__key函数而产生的少量开销外,这几乎与任何哈希一样快。当然,如果已知这些属性是整数,并且它们的数量不是太多,我想您可以通过一些本地滚动的散列来稍微加快运行速度,但是它可能分布得不那么理想。hash((self.attr_a, self.attr_b, self.attr_c))由于将特别优化优化小s的创建,因此它将以惊人的速度(并且正确),tuple并且将获取和组合哈希的工作推到了C内置函数中,这通常比Python级别的代码要快。
ShadowRanger

假设将类A的对象用作字典的键,并且如果类A的属性发生更改,则其哈希值也将更改。那不会造成问题吗?
Matrix先生

1
就像@ loved.by.Jesus在下面的回答中提到的那样,不应为可变对象定义/覆盖哈希方法(默认情况下定义,并使用id进行相等性和比较)。
Matrix先生

@Miguel,我遇到了确切的问题None一旦键更改,字典将返回。我解决该问题的方法是将对象的ID存储为键而不是仅存储对象。
Jaswant P

@JaswantP默认情况下,Python使用对象的ID作为任何可哈希对象的键。
Matrix先生

22

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

此解决方案的问题是hash(A(a, b, c)) == hash((a, b, c))。换句话说,哈希与它的关键成员的元组冲突。也许这在实践中并不经常发生?

更新:Python文档现在建议使用上面的示例中的元组。请注意,文档说明

唯一需要的属性是比较相等的对象具有相同的哈希值

注意相反的说法是不正确的。不相等的对象可能具有相同的哈希值。这种哈希冲突在用作dict键或set元素时不会导致一个对象替换另一对象,只要这些对象也不能相等

过时/不好的解决方案

上的Python文档__hash__建议使用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)))

更新:正如Blckknght指出的那样,更改a,b和c的顺序可能会引起问题。我添加了一个附加项^ hash((self._a, self._b, self._c))来捕获被哈希值的顺序。^ hash(...)如果合并的值无法重新排列(例如,如果它们的类型不同,因此_a将永远不会将的值分配给_b_c,等等),则可以删除此最终形式。


5
通常,您不希望对属性直接进行异或,因为如果更改值的顺序,则会产生冲突。也就是说,hash(A(1, 2, 3))将等于hash(A(3, 1, 2))(他们俩哈希等于任何其他A情况下用的置换123作为其值)。如果要避免实例与其参数元组具有相同的哈希,只需创建一个哨兵值(作为类变量或全局变量),然后将其包含在要哈希的元组中:return hash((__ sentinel ,self._a,self._b,self._c))
Blckknght

1
您对的使用isinstance可能会出现问题,因为的子类的对象type(self)现在可以等于的对象type(self)。因此,您可能会发现,将a Car和a 添加Ford到a set()可能只会插入一个对象,具体取决于插入的顺序。此外,您可能会遇到“ a == b真”但“ b == a假”的情况。
MaratC

1
如果要子类化B,则可能需要将其更改为isinstance(othr, B)
millerdev

7
一个想法:键元组可以包括类类型,这将防止具有相同键集属性的其他类显示为相等:hash((type(self), self._a, self._b, self._c))
Ben Mosher

2
除了关于使用B代替的要点外type(self),通常还会考虑NotImplemented__eq__代替in遇到意外类型时返回,这是更好的做法False。这样,其他用户定义的类型就可以实现__eq__了解B并可以与之进行比较的,如果他们愿意的话。
Mark Amery

16

Microsoft Research的Paul Larson研究了各种哈希函数。他告诉我

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

对于各种各样的琴弦,效果都非常好。我发现类似的多项式技术可以很好地用于计算不同子字段的哈希。


8
显然,Java使用相同的方法,但是使用31而不是101
user229898 2010年

3
使用这些数字的依据是什么?是否有理由选择101或31?
2013年

1
以下是素数乘法器的解释:stackoverflow.com/questions/3613102/…。根据保罗·拉森(Paul Larson)的实验,101似乎工作得特别好。
乔治五世赖利

4
Python将(hash * 1000003) XOR ord(c)字符串用于32位环绕式乘法。[引文 ]
tylerl

4
即使这是真的,在这种情况下也没有实际用处,因为内置的Python字符串类型已经提供了一种__hash__方法。我们不需要自己动手。问题是如何__hash__为典型的用户定义类(具有指向内置类型或其他此类用户定义类的属性)实现,此答案根本无法解决。
Mark Amery

3

我可以尝试回答您问题的第二部分。

冲突可能不是哈希码本身引起的,而是哈希码映射到集合中的索引所导致的。因此,例如,您的哈希函数可以返回1到10000之间的随机值,但是如果您的哈希表只有32个条目,则在插入时会发生冲突。

另外,我认为冲突将由集合内部解决,并且有很多解决冲突的方法。最简单(也是最糟糕)的情况是,给定要插入到索引i的条目,将i加1直到找到一个空白点并插入该位置。然后,检索以相同的方式进行。这会导致某些条目的检索效率低下,因为您可能有一个条目需要遍历整个集合才能找到!

其他冲突解决方法通过在插入项目以散布事物时移动哈希表中的条目来减少检索时间。这会增加插入时间,但假定您阅读的内容多于插入内容。也有尝试将不同的冲突条目分支出来的方法,以使条目聚集在一个特定位置。

另外,如果您需要调整集合的大小,则需要重新哈希所有内容或使用动态哈希方法。

简而言之,根据您使用的哈希码,您可能必须实现自己的冲突解决方法。如果不将它们存储在集合中,则可以使用仅生成很大范围内的哈希码的哈希函数来解决。如果是这样,则可以根据您的内存问题来确保容器大于所需的容器(当然,容器越大越好)。

如果您有更多兴趣,请点击以下链接:

维基百科上的合并哈希

Wikipedia还总结了各种冲突解决方法:

此外,Tharp的“ 文件组织和处理 ”广泛涵盖了许多冲突解决方法。IMO是哈希算法的重要参考。


1

__hash__programiz网站上很好地解释了何时以及如何实现该功能:

只是一个截图以提供概述:(检索2019-12-13)

https://www.programiz.com/python-programming/methods/built-in/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))

0

取决于您返回的哈希值的大小。这是很简单的逻辑,如果您需要基于四个32位int的哈希值返回32位int,则会发生冲突。

我希望位操作。像下面的C伪代码:

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

如果仅将它们用作浮点值而不是实际代表浮点值,则这样的系统也可以用于浮点数,也许更好。

对于字符串,我几乎一无所知。


我知道会有碰撞。但是我不知道如何处理这些信息。而且,我的属性值组合非常稀疏,因此我一直在寻找一种明智的解决方案。我以某种方式期望某处有最佳实践。
user229898
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.