正如许多人所指出的那样,Python的hash
不再是一致的(从3.3版开始),因为默认情况下现在默认使用随机PYTHONHASHSEED
(以解决安全问题,如{{3中所述) }}。
但是,我注意到某些对象的哈希值仍然是一致的(无论如何从Python 3.7开始):包括int
,float
,tuple(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实例启动,就无法更改它。
我尝试过的事情
我可以使用hashlib.sha1
或类似方法来计算一致的哈希值。这比较慢,而且我不能直接使用frozenset
技巧:更新哈希器时,我必须以一致的顺序遍历dict(例如,通过对键进行排序,比较慢)。在对真实数据的测试中,我发现速度降低了50倍以上。
我可以尝试对每个商品获得的一致哈希应用定序哈希算法(这也很慢,因为为每个商品启动新的哈希很耗时)。
我可以尝试将所有内容转换为int或int元组,然后将其转换为此类的setset。目前,似乎所有int
,tuple(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)
答案 0 :(得分:2)
对广泛问题的简短回答:除了x == y
要求hash(x) == hash(y)
的总体保证外,没有关于哈希稳定性的明确保证 。暗示x
和y
都是在程序的同一运行中定义的(您无法执行x == y
,因为其中一个显然不存在于该程序中,因此不需要保证每次运行的哈希值。
对特定问题的更长回答:
[您相信
int
,float
,tuple(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),因此哈希无法在具有不同解释器的计算机之间移植。
对tuple
和frozenset
的算法不做任何保证;我无法想到他们会在运行之间更改它的任何原因(如果基础类型是种子,则tuple
和frozenset
会从中受益而无需进行任何更改),但是他们可以并且确实会更改CPython版本之间的实现(例如in late 2018 they made a change to reduce the number of hash collisions in short tuple
s of int
s and float
s),因此,如果您存储了tuple
的哈希值(例如3.7),然后在3.8+中计算相同的tuple
的哈希值,则它们将不匹配(即使它们在3.7运行之间或3.8运行之间也匹配)。
如果是这样,那会保持这种趋势吗?
预期为是。保证,没有。我可以很容易地看到int
的种子哈希(并且通过扩展,可以保留所有数字类型以保留数字哈希/等式保证),原因与他们为str
/ bytes
注入哈希的原因相同等等。主要障碍是:
int
转换为str
之前将其用作密钥)。
PYTHONHASHSEED
是否仅用于加盐字符串和字节数组的哈希值?
除了str
和bytes
之外,它还适用于许多随机事物,它们根据str
或bytes
的散列实现自己的散列,通常是因为它们已经可以自然地转换为原始字节,并且通常在面向Web的前端填充的dict
中用作密钥。我所知道的临时组件包括datetime
模块的各种类(datetime
,date
,time
,尽管实际上并没有记录在模块本身中)和只读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
的作用没有明显不同,但是更易于管理。