【问题标题】:What's a correct and good way to implement __hash__()?什么是实现 __hash__() 的正确和好方法?
【发布时间】:2011-02-23 22:40:48
【问题描述】:

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

我说的是返回哈希码的函数,该哈希码随后用于将对象插入哈希表(也称为字典)中。

由于__hash__() 返回一个整数并用于将对象“分箱”到哈希表中,我假设返回的整数值应该均匀分布在公共数据中(以尽量减少冲突)。 获得这些值的好习惯是什么?碰撞有问题吗? 就我而言,我有一个小类,它充当容器类,包含一些整数、一些浮点数和一个字符串。

【问题讨论】:

    标签: python hashtable hashcode dictionary


    【解决方案1】:

    实现散列(以及列表、字典、元组)的一种好方法是通过使用__iter__ 使其可迭代来使对象具有可预测的项目顺序。所以从上面修改一个例子:

    class A(object):
    
        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__不需要hash,但是很容易实现)。

    现在添加一些可变成员来看看它是如何工作的:

    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'
    

    【讨论】:

      【解决方案2】:

      关于何时以及如何实现__hash__ 函数的一个很好的解释在programiz website

      只是提供概述的屏幕截图: (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))
      

      【讨论】:

        【解决方案3】:

        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 文档现在建议使用上面示例中的元组。请注意,文档说明

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

        请注意,相反的情况并非如此。比较不相等的对象可能具有相同的哈希值。这种哈希冲突不会导致一个对象在用作字典键或集合元素时替换另一个对象只要对象不也比较相等

        过时/糟糕的解决方案

        Python documentation on __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)) 来捕获被散列的值的顺序。如果无法重新排列组合的值(例如,如果它们具有不同的类型,因此_a 的值将永远不会分配给_b_c 等),则可以删除最后的^ hash(...)

        【讨论】:

        • 您通常不希望将属性直接异或在一起,因为如果您更改值的顺序,这会给您带来冲突。也就是说,hash(A(1, 2, 3)) 将等于 hash(A(3, 1, 2))(并且它们都将等于任何其他 A 实例的哈希值,其值为 123 的排列)。如果您想避免您的实例具有与其参数的元组相同的散列,只需创建一个哨兵值(作为类变量或作为全局变量)然后将其包含在要散列的元组中: return hash((_sentinel , self._a, self._b, self._c))
        • 您对isinstance 的使用可能会出现问题,因为type(self) 子类的对象现在可以等于type(self) 的对象。因此,您可能会发现将CarFord 添加到set() 可能会导致仅插入一个对象,具体取决于插入顺序。此外,您可能会遇到a == b 为真但b == a 为假的情况。
        • 如果您要继承 B,您可能需要将其更改为 isinstance(othr, B)
        • 一个想法:键元组可以包含类类型,这将防止具有相同键属性集的其他类被显示为相等:hash((type(self), self._a, self._b, self._c))
        • 除了使用B 代替type(self) 之外,当遇到__eq__ 中的意外类型而不是False 时,通常认为返回NotImplemented 是更好的做法。这允许 其他 用户定义的类型实现知道 B__eq__ 并且可以与它进行比较,如果他们愿意的话。
        【解决方案4】:

        实现__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
        

        另外,documentation of __hash__ 包含更多信息,在某些特定情况下可能很有价值。

        【讨论】:

        • 除了分解出 __key 函数的小开销之外,这几乎与任何散列一样快。当然,如果已知属性是整数,并且它们的数量不多,我想您可能会使用一些自制散列来稍微更快地运行,但它可能不会以及分布。 hash((self.attr_a, self.attr_b, self.attr_c)) 将会非常快(并且正确),因为小型tuples 的创建经过特别优化,它将获取和组合散列的工作推向了 C 内置函数,这通常是比 Python 级别的代码更快。
        • 假设 A 类的对象被用作字典的键,如果 A 类的属性发生变化,其哈希值也会发生变化。这不会造成问题吗?
        • 正如@loved.by.Jesus 在下面的回答中提到的,不应该为可变对象定义/覆盖哈希方法(默认定义并使用 id 进行相等和比较)。
        • @Miguel,我遇到了确切的problem,一旦密钥更改,字典就会返回None。我解决它的方法是将对象的 id 存储为键,而不仅仅是对象。
        • @JaswantP Python 默认使用对象的 id 作为任何可散列对象的键。
        【解决方案5】:

        取决于您返回的哈希值的大小。这是一个简单的逻辑,如果你需要基于四个 32 位整数的哈希返回一个 32 位整数,你会遇到冲突。

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

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

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

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

        【讨论】:

        • 我知道会有碰撞。但我不知道这些是如何处理的。此外,我的属性值组合起来非常稀疏,所以我一直在寻找一个聪明的解决方案。不知何故,我希望在某个地方有一个最佳实践。
        【解决方案6】:

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

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

        对于各种各样的字符串来说效果出奇的好。我发现类似的多项式技术适用于计算不同子字段的哈希。

        【讨论】:

        • 显然 Java 也是这样做的,但使用 31 而不是 101
        • 使用这些数字的原因是什么?有理由选择 101 还是 31?
        • 这里是对素数乘数的解释:stackoverflow.com/questions/3613102/…。根据 Paul Larson 的实验,101 似乎效果特别好。
        • Python 使用 (hash * 1000003) XOR ord(c) 处理具有 32 位环绕乘法的字符串。 [Citation]
        • 即使这是真的,在这种情况下也没有实际用处,因为内置的 Python 字符串类型已经提供了 __hash__ 方法;我们不需要自己动手。问题是如何为典型的用户定义类实现__hash__(有一堆属性指向内置类型或可能指向其他此类用户定义类),这个答案根本没有解决。
        【解决方案7】:

        我可以试着回答你问题的第二部分。

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

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

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

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

        简而言之,根据您使用的哈希码,您可能需要实现自己的冲突解决方法。如果您没有将它们存储在一个集合中,您可能会使用一个哈希函数,该函数只会在一个非常大的范围内生成哈希码。如果是这样,您可以确保您的容器比它需要的更大(当然越大越好),具体取决于您的内存问题。

        如果您更感兴趣,这里有一些链接:

        coalesced hashing on wikipedia

        维基百科也有各种冲突解决方法的summary

        此外,Tharp 的“File Organization And Processing”广泛涵盖了许多冲突解决方法。 IMO 它是散列算法的一个很好的参考。

        【讨论】:

          猜你喜欢
          • 2011-11-06
          • 2018-08-12
          • 1970-01-01
          • 2011-03-27
          • 1970-01-01
          • 2017-12-23
          • 1970-01-01
          • 2018-02-20
          • 2011-05-04
          相关资源
          最近更新 更多