Python一致性哈希替换

5
许多人指出,Python的hash从3.3版本开始不再一致,因为默认情况下使用随机的PYTHONHASHSEED(这是为了解决安全问题,如this excellent answer所述)。
然而,我注意到某些对象的哈希仍然是一致的(至少在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只适用于给字符串和字节数组散列加盐吗?
为什么我要问这个问题?
我有一个依赖哈希记住是否已经看到给定的字典(任何顺序)的系统:{key: tuple(ints)}。在该系统中,键是文件名的集合,元组是与它们相关联的os.stat_result的子集,例如(size, mtime)。该系统用于基于检测差异进行更新/同步决策。
在我的应用程序中,我有大约10万个这样的字典,每个字典可以表示数千个文件及其状态,因此缓存的紧凑性非常重要。
我可以容忍小的误报率(64位哈希值可能会出现<10^-19的碰撞率,另请参见birthday paradox)。
每个这样字典的一种紧凑表示如下所示:“fsd”:
def fsd_hash(fsd: dict):
    return hash(frozenset(fsd.items()))

这个方法非常快速,使用单个整数来表示整个字典(并且不受顺序影响)。如果 fsd 字典中的任何内容发生变化,则哈希值很可能会不同。

不幸的是,hash 只在单个 Python 实例内部一致,对于主机比较其各自的哈希值而言毫无用处。将完整的缓存 ({location_name: fsd_hash}) 持久化到磁盘上以便重新加载也没有用。

我不能指望使用该模块的更大系统已经使用了 PYTHONHASHSEED=0,而且据我所知,一旦 Python 实例启动,就没有办法更改它。

我尝试过的事情

  1. 我可以使用hashlib.sha1或类似的方法来计算一致的哈希值。这会更慢,而且我不能直接使用frozenset技巧:我必须通过一个一致的顺序(例如按键排序,速度较慢)遍历字典并更新哈希器。在我的实际数据测试中,我看到了超过50倍的减速。

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

  3. 我可以尝试将所有内容转换为整数或整数元组,然后将其冻结为元组的集合。目前,似乎所有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
为什么需要哈希在运行中保持一致?这听起来像是“XY问题”。字典甚至不能哈希,因此“字典的一致哈希”是毫无意义的,任何依赖哈希在运行间保持一致的代码都误解了哈希的目的。这也让我怀疑您是否期望哈希是唯一的,这根本就不是保证。 - ShadowRanger
1
@jsbueno:我认为那样做行不通;他想要对于frozensetdict进行无序哈希,但是将它们pickle的结果将是有序的(必须按照特定顺序写出项目,即使集合彼此相等,由于构造顺序不同,顺序也可能不同)。 - ShadowRanger
相关的,hash 是非持久性的:https://dev59.com/QF4c5IYBdhLWcg3wzs6Q - Albert
相关的,从元组中获取持久哈希值:https://dev59.com/z7Dma4cB1Zd3GeqPArza - Albert
与嵌套冻结集相关的持久哈希:https://dev59.com/z17Va4cB1Zd3GeqPLqj- - Albert
显示剩余5条评论
1个回答

2
对广泛问题的简短回答:除了总体保证“x == y要求hash(x) == hash(y)”之外,没有明确的哈希稳定性保证。这意味着xy都在程序的同一运行中定义(显然不能在一个不存在的程序中执行x == y,因此不需要对跨运行的哈希进行任何保证)。
对特定问题的更长回答:
“您认为intfloattuple(x)frozenset(x)(对于具有一致哈希的x)是否始终如此,并且是否有保证?”
这是关于数字类型的真实情况,机制已经正式记录下来,但是该机制仅适用于特定解释器的特定版本。sys.hash_info提供了各种常量, 它们在该解释器上是一致的,但在不同的解释器上(如CPython vs. PyPy,64位构建 vs. 32位构建,甚至3.n vs. 3.n+1),它们可能会有所不同(在64位和32位CPython的情况下已记录差异),因此哈希值不能在具有不同解释器的计算机之间移植。
对于元组和冻结集合,算法没有任何保证;我想不出为什么它们会在运行之间更改(如果基础类型是种子的,则元组和冻结集合从中受益而不需要任何更改),但它们可以在CPython版本更新之间更改实现(例如,在2018年末,他们对int和float的短元组中的哈希冲突数量进行了减少)。因此,如果您保存了来自3.7的元组哈希值,然后在3.8+中计算相同元组的哈希值,它们将不匹配(即使它们在3.7或3.8上的多次运行之间匹配)。

如果是这样,是否有望保持这种状态?

那么,是否期望保持这种状态?
预计会这样做,但不能保证。我可以很容易地看到对于整数(以及所有数字类型的推广,以保留数字哈希/相等性的保证),它们会像为字符串/字节串等种子哈希一样进行种子哈希。主要障碍将是:
  1. 它几乎肯定比当前非常简单的算法慢。
  2. 通过明确记录数字哈希算法,他们需要一个漫长的弃用期,在此之前他们无法更改它。
  3. 这并不是严格必要的(如果Web应用程序需要种子哈希来防止DoS攻击,则可以在使用它们作为键之前将整数转换为字符串)。

PYTHONHASHSEED 只适用于加盐字符串和字节数组的哈希吗?

除了 strbytes 之外,它还适用于许多实现自己的哈希方式的随机事物,通常是因为它们已经自然地转换为原始字节并且通常用作由面向Web的前端填充的 dict 中的键。我知道的包括 datetime 模块的各种类(datetimedatetime,尽管这在模块本身中并没有得到记录),以及只读的具有字节大小格式的 memoryview(其 哈希等效于哈希视图的 .tobytes() 方法的结果)。

dict 包含各种类型和类时,如何编写一种一致的哈希替换方法来替代 hash(frozenset(some_dict.items()))

最简单/最可组合的解决方案可能是将您的const_hash定义为单个调度函数, 并像使用hash本身一样使用它。这避免了在一个单一位置定义必须处理所有类型的单个函数; 您可以在中心位置具有const_hash默认实现(仅依赖于已知一致哈希的内容的hash),并在那里提供内置类型的附加定义,您知道它们不一致(或可能包含不一致的内容),同时仍然允许人们通过导入您的const_hash并使用@const_hash.register装饰其类型的实现来无缝扩展它所涵盖的事物集。它在效果上与您提出的const_hash没有显著区别,但更易管理。

非常好的回复,特别是最后一段正是我所需要的。我不知道单分派函数。这确实使设计更加美观:可组合和可扩展。 - Pierre D

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接