如何在Python3中组合哈希代码



我更熟悉从子类中的超类构建复杂/组合哈希代码的"Java方式"。Python 3中有更好/不同/首选的方法吗?(我在谷歌上找不到任何关于Python3的具体信息。)

class Superclass:
def __init__(self, data):
self.__data = data
def __hash__(self):
return hash(self.__data)
class Subclass(Superclass):
def __init__(self, data, more_data):
super().__init__(data)
self.__more_data = more_data
def __hash__(self):
# Just a guess...
return hash(super()) + 31 * hash(self.__more_data)

为了简化这个问题,请假设self.__dataself.__more_data是简单的、可散列的数据,例如strint

生成好的哈希的最简单方法是将您的值放入标准的可哈希Python容器中,然后对进行哈希。这包括在子类中组合散列。我将解释为什么,然后如何

基本要求

第一件事:

  • 如果两个对象测试为相等,则它们必须具有相同的哈希值
  • 具有散列的对象必须随着时间的推移产生相同的散列

只有遵循这两条规则,您的对象才能安全地在字典和集合中使用。哈希不改变是防止字典和集合崩溃的原因,因为它们使用哈希来选择存储位置,并且如果哈希改变,则在给定另一个测试结果相同的对象的情况下,将无法再次定位该对象。

请注意,这两个对象是否属于不同的类型都无关紧要;因此True == 1 == 1.0都具有相同的散列,并且都将作为字典中的相同密钥计数。

什么是一个好的哈希值

您希望将对象值的组件组合在一起,以尽可能为不同的值生成不同的哈希。这包括排序特定含义等内容,因此,表示值的不同方面但可以容纳相同类型Python对象的两个属性在大多数情况下仍然会产生不同的散列。

请注意,如果表示不同值(测试不相等)的两个对象具有相等的哈希值,则很好。重用哈希值不会破坏集合或字典。然而,如果许多不同的对象值产生相同的哈希,则会降低它们的效率,因为会增加碰撞的可能性。冲突需要冲突解决,而冲突解决需要更多的时间,因此您可以在具有可预测哈希实现的服务器上使用拒绝服务攻击)(*)

所以你想要一个很好的广泛的可能的散列值。

需要注意的陷阱

object.__hash__方法的文档包括一些关于如何组合值的建议:

唯一需要的属性是比较相等的对象具有相同的哈希值;建议以某种方式将对象的组件的哈希值混合在一起(例如,使用exclusive或),这些组件也在对象的比较中发挥作用。

仅使用XOR将不会产生良好的哈希值,当您将其哈希进行XOR的值一起使用时,这些值可以是相同的类型,但根据它们被分配的属性具有不同的含义。举例说明:

>>> class Foo:
...     def __init__(self, a, b):
...         self.a = a
...         self.b = b
...     def __hash__(self):
...         return hash(self.a) ^ hash(self.b)
...
>>> hash(Foo(42, 'spam')) == hash(Foo('spam', 42))
True

因为self.aself.b的散列只是异或在一起,所以我们得到了两个顺序相同的散列值,因此有效地将可用散列的数量减半。使用更多的属性可以快速减少唯一散列的数量。因此,如果组成哈希的不同元素中可以使用相同的值,那么您可能希望在哈希中包含更多关于每个属性的信息。

接下来,要知道,虽然Python整数是无界的,但哈希值不是。也就是说,散列值的范围是有限的。来自同一文档:

注意hash()将从对象的自定义__hash__()方法返回的值截断为Py_ssize_t的大小。在64位构建中通常为8字节,在32位构建中为4字节。

这意味着,如果使用加法或乘法或其他操作来增加存储哈希值所需的位数,则最终会丢失高位,从而再次减少不同哈希值的数量。

接下来,如果您将多个哈希与XOR组合在一起,这些哈希的范围已经很有限,那么最终可能会得到更少数量的哈希。举个极端的例子,试着对0-10范围内的1000个随机整数的散列进行XOR运算。

哈希,简单的方法

Python开发人员早就与上述陷阱作斗争,并为标准库类型解决了这一问题。利用这个优势将您的值放入一个元组,然后对该元组进行散列。

Python元组使用xxHash算法的简化版本来捕获订单信息,并确保哈希值范围广泛。因此,对于不同的属性,您可以通过在元组中给它们不同的位置来捕捉不同的含义,然后对元组进行散列:

def __hash__(self):
return hash((self.a, self.b))

这样可以确保为唯一的排序获得唯一的哈希值。

如果您正在对某个东西进行子类化,请将父实现的哈希放入元组位置之一:

def __hash__(self):
return hash((super().__hash__(), self.__more_data))

哈希值确实会将其减少到60位或30位(分别在32位或64位平台上),但当与元组中的其他值组合时,这并不是什么大问题。如果您真的很关心这一点,那么将None作为占位符放在元组中,并对父散列进行XOR运算(因此super().__hash__() ^ hash((None, self.__more_data)))。但这真的太夸张了。

如果有多个值的相对顺序无关紧要,并且不想将它们逐一异或,请考虑使用frozenset()对象进行快速处理,如果值不是唯一的,请将其与collections.Counter()对象组合使用。frozenset()散列运算通过首先重新排列散列中的位来解决小的散列范围:

# unordered collection hashing
from collections import Counter
hash(frozenset(Counter(...).items()))

一如既往,元组或frozenset()中的所有值本身都必须是可散列的。

考虑使用数据类

对于为其编写__hash__函数的大多数对象,您实际上希望使用dataclass生成的类:

from dataclasses import dataclass
from typing import Union
@dataclass(frozen=True)
class Foo:
a: Union[int, str]
b: Union[int, str]

frozen=Trueunsafe_hash=True使用所有字段值的tuple()时,为数据类提供了一个合理的__hash__实现。


(*)Python通过使用进程范围的随机哈希种子对字符串、字节和datetime对象进行哈希处理,保护您的代码免受此类哈希冲突攻击。

python文档建议您使用xor来组合散列:

唯一需要的属性是比较相等的对象具有相同的哈希值;建议以某种方式将对象的组件的哈希值混合在一起(例如,使用exclusive或),这些组件也在对象的比较中发挥作用。

我也建议xor而不是加法和乘法,因为这一点:

注意

hash()将从对象的自定义__hash__()方法返回的值截断为Py_ssize_t的大小。在64位构建中通常为8字节,在32位构建中为4字节。如果对象的__hash__()必须在不同位大小的构建上进行互操作,请确保检查所有支持的构建的宽度。一种简单的方法是使用CCD_;

顺便说一句,本文档与python 2.7和python 3.4相同。

关于对称性和异环项自身的一个注记

正如评论中所指出的,xor是对称的,因此操作顺序消失了。两个相同元素的异或也是零。所以,如果在某些旋转或移位中不需要混合,或者更好的是,使用该解决方案的建议,即获取标识元素的元组的散列。如果您不想保持秩序,可以考虑使用frozenset

不要将多个字符串组合在一起,而是使用元组,因为它们在python中是可哈希的。

t: Tuple[str, str, int] = ('Field1', 'Field2', 33)
print(t.__hash__())

这将使代码更易于阅读。

对于阅读本文的人来说,XORing哈希是个坏主意,因为一个特定的重复哈希值序列可能会异或在一起,并有效地从哈希集中删除一个元素。

例如:

(hash('asd') ^ hash('asd') ^ hash('derp')) == hash('derp')

甚至:

(hash('asd') ^ hash('derp') ^ hash('asd')) == hash('derp')

因此,如果你使用这种技术来确定某组值是否在组合哈希中,其中你可能会在哈希中添加重复的值,那么使用XOR可能会导致该值从集合中删除。相反,您应该考虑OR,它具有与前面海报提到的避免无限整数增长的相同特性,但确保不会删除重复项。

(hash('asd') | hash('asd') | hash('derp')) != hash('derp')

如果你想进一步探索这一点,你应该查找Bloom过滤器。

最新更新