Python一致的哈希替换

时间:2020-10-13 23:30:03

标签: python hash

正如许多人所指出的那样,Python的hash不再是一致的(从3.3版开始),因为默认情况下现在默认使用随机PYTHONHASHSEED(以解决安全问题,如{{3中所述) }}。

但是,我注意到某些对象的哈希值仍然是一致的(无论如何从Python 3.7开始):包括intfloattuple(x)frozenset(x) (只要x产生一致的哈希值)。例如:

assert hash(10) == 10
assert hash((10, 11)) == 3713074054246420356
assert hash(frozenset([(0, 1, 2), 3, (4, 5, 6)])) == -8046488914261726427

这是否一直都是正确的并得到保证?如果是这样,那会保持这种趋势吗? PYTHONHASHSEED是否仅用于加盐字符串和字节数组的哈希值?

我为什么要问?

我有一个系统依靠哈希来记住我们是否看到了给定的dict(以任何顺序):{key: tuple(ints)}。在该系统中,键是文件名的集合,而元组是os.stat_result的子集,例如(size, mtime)与他们相关联。该系统用于根据检测到的差异做出更新/同步决策。

在我的应用程序中,我有大约10万个这样的命令,每个命令可以代表数千个文件及其状态,因此缓存的紧凑性很重要。

我可以容忍来自可能的哈希冲突(请参见this excellent answer)的较小的误报率(对于64位哈希,为<10 ^ -19)。

对于每个这样的字典“ fsd”,以下是一个紧凑的表示形式:

def fsd_hash(fsd: dict):
    return hash(frozenset(fsd.items()))

它非常快,并且产生一个int来表示整个字典(具有顺序不变性)。如果fsd字典中的任何内容发生变化,则散列很有可能会不同。

不幸的是,hash仅在单个Python实例中是一致的,从而使主机无法比较它们各自的哈希值。将完整的缓存({location_name: fsd_hash})保留在磁盘上以在重新启动时重新加载也是没有用的。

我不能期望使用PYTHONHASHSEED=0调用了使用该模块的更大系统,并且据我所知,一旦Python实例启动,就无法更改它。

我尝试过的事情

  1. 我可以使用hashlib.sha1或类似方法来计算一致的哈希值。这比较慢,而且我不能直接使用frozenset技巧:更新哈希器时,我必须以一致的顺序遍历dict(例如,通过对键进行排序,比较慢)。在对真实数据的测试中,我发现速度降低了50倍以上。

  2. 我可以尝试对每个商品获得的一致哈希应用定序哈希算法(这也很慢,因为为每个商品启动新的哈希很耗时)。

  3. 我可以尝试将所有内容转换为int或int元组,然后将其转换为此类的setset。目前,似乎所有inttuple(int)frozenset(tuple(int))都产生一致的哈希值,但是:是否可以保证,如果可以,我可以期望这种情况持续多久?

其他问题:更广泛地说,当字典包含各种类型和类时,为hash(frozenset(some_dict.items()))编写一致的哈希替换的一种好方法是什么?我可以为自己拥有的类实现自定义__hash__(一个一致的),但是例如,我不能覆盖str的哈希。我想到的一件事是:

def const_hash(x):
    if isinstance(x, (int, float, bool)):
        pass
    elif isinstance(x, frozenset):
        x = frozenset([const_hash(v) for v in x])
    elif isinstance(x, str):
        x = tuple([ord(e) for e in x])
    elif isinstance(x, bytes):
        x = tuple(x)
    elif isinstance(x, dict):
        x = tuple([(const_hash(k), const_hash(v)) for k, v in x.items()])
    elif isinstance(x, (list, tuple)):
        x = tuple([const_hash(e) for e in x])
    else:
        try:
            return x.const_hash()
        except AttributeError:
            raise TypeError(f'no known const_hash implementation for {type(x)}')
    return hash(x)

1 个答案:

答案 0 :(得分:2)

对广泛问题的简短回答:除了x == y要求hash(x) == hash(y)的总体保证外,没有关于哈希稳定性的明确保证 。暗示xy都是在程序的同一运行中定义的(您无法执行x == y,因为其中一个显然不存在于该程序中,因此不需要保证每次运行的哈希值。

对特定问题的更长回答:

[您相信intfloattuple(x)frozenset(x)(对于x,具有一致的哈希值)在各个运行中具有一致的哈希值)总是真实和保证?

使用the mechanism being officially documented的数字类型是正确的,但是仅针对特定构建的特定解释器保证该机制。 sys.hash_info provides the various constants,它们将保持一致在该解释器上,但是在不同的解释器上(CPython与PyPy,64位构建与32位构建,甚至3.n与3.n + 1),它们可以有所不同(在64与32位版本之间存在差异。 32位CPython),因此哈希无法在具有不同解释器的计算机之间移植。

tuplefrozenset的算法不做任何保证;我无法想到他们会在运行之间更改它的任何原因(如果基础类型是种子,则tuplefrozenset会从中受益而无需进行任何更改),但是他们可以并且确实会更改CPython版本之间的实现(例如in late 2018 they made a change to reduce the number of hash collisions in short tuples of ints and floats),因此,如果您存储了tuple的哈希值(例如3.7),然后在3.8+中计算相同的tuple的哈希值,则它们将不匹配(即使它们在3.7运行之间或3.8运行之间也匹配)。

如果是这样,那会保持这种趋势吗?

预期为是。保证,没有。我可以很容易地看到int的种子哈希(并且通过扩展,可以保留所有数字类型以保留数字哈希/等式保证),原因与他们为str / bytes注入哈希的原因相同等等。主要障碍是:

  1. 几乎可以肯定它会比当前非常简单的算法慢。
  2. 通过显式记录数字哈希算法,他们需要很长一段时间不赞成使用,然后才能对其进行更改。
  3. 这不是绝对必要的(如果Web应用程序需要种子散列来进行DoS保护,它们始终可以在将int转换为str之前将其用作密钥)。

PYTHONHASHSEED是否仅用于加盐字符串和字节数组的哈希值?

除了strbytes之外,它还适用于许多随机事物,它们根据strbytes的散列实现自己的散列,通常是因为它们已经可以自然地转换为原始字节,并且通常在面向Web的前端填充的dict中用作密钥。我所知道的临时组件包括datetime模块的各种类(datetimedatetime,尽管实际上并没有记录在模块本身中)和只读memoryview字节大小的格式(hash equivalently to hashing the result of the view's .tobytes() method)。

hash(frozenset(some_dict.items()))包含各种类型和类时,为dict编写一致的哈希替换的好方法是什么?

最简单/最可组合的解决方案可能是将const_hash定义为a single dispatch function,并使用与自己hash相同的方式。这样可以避免在一个地方定义一个必须处理所有类型的单一函数。您可以将const_hash的默认实现(对于具有已知一致哈希的事物仅依赖于hash)放置在中央位置,并为您知道不一致的内置类型提供其他定义(或其中可能包含不一致的内容),同时仍然允许人们通过导入const_hash注册自己的单调度函数并用{{1装饰其类型的实现,从而无缝扩展其涵盖的范围。 }}。与您建议的@const_hash.register的作用没有明显不同,但是更易于管理。

相关问题