有什么理由*不*缓存对象的哈希值吗?

18 投票
3 回答
3499 浏览
提问于 2025-04-16 04:33

我写了一个类,它的 .__hash__() 方法执行起来很慢。我在考虑缓存它的哈希值,把它存储在一个像 ._hash 这样的变量里,这样 .__hash__() 方法就可以直接返回 ._hash。这个哈希值会在 .__init__() 方法结束时计算,或者在第一次调用 .__hash__() 的时候计算。

我这么想是因为:“这个对象是不可变的 -> 它的哈希值永远不会改变 -> 所以我可以缓存这个哈希值。”

但现在我又在想:其实你可以对任何可哈希的对象这么说。(除了那些哈希值就是它们 ID 的对象。)

那么,除了那些哈希计算非常快的小对象,是否还有理由不缓存一个对象的哈希值呢?

3 个回答

-1

通常情况下,Python中的大多数对象都是可变的,也就是说它们的内容可以被改变。所以如果一个对象的哈希值是根据它的属性计算出来的,一旦你改变了这些属性,哈希值也会随之改变。如果你的类确实是不可变的,并且所有参与计算哈希值的属性也是不可变的,那么你就可以把哈希值存起来,避免每次都重新计算。

2

之前提到过,在某些情况下,缓存哈希值可能是个不错的选择。需要注意的是,你不能直接使用@functools.cache@functools.lru_cache,你需要自己实现缓存功能。原因是这些装饰器要求被装饰的函数的参数必须是可哈希的,包括self,这就导致了递归定义的问题。

所以,如果你看到下面的代码报错,不要感到奇怪:

from functools import cache, lru_cache

class Obj():
    @cache
    def __hash__(self):
        return 42  # or anything else really

o = Obj()
print(hash(o))

调用hash(o)时会出现以下错误:

RecursionError: maximum recursion depth exceeded while calling a Python object

这是因为Obj.__hash__的缓存首先会检查它之前是否见过参数self=o,因此它会在内部字典中查找键o(或者可能是一个包含o的元组,我不太确定,但细节不重要),然后再调用Obj.__hash__(o)

我花了一些时间才弄明白为什么会出现这个错误,我想那些被引导到这里的人可能也会对此感兴趣。

15

当然,缓存哈希值是可以的。实际上,Python 对字符串本身就会这样做。这里的权衡是哈希计算的速度和保存哈希值所占的空间之间的关系。比如说,这就是为什么元组不缓存它们的哈希值,而字符串会缓存的原因(可以参考 增强请求 #1462796)。

撰写回答