有什么理由*不*缓存对象的哈希值吗?
我写了一个类,它的 .__hash__()
方法执行起来很慢。我在考虑缓存它的哈希值,把它存储在一个像 ._hash
这样的变量里,这样 .__hash__()
方法就可以直接返回 ._hash
。这个哈希值会在 .__init__()
方法结束时计算,或者在第一次调用 .__hash__()
的时候计算。
我这么想是因为:“这个对象是不可变的 -> 它的哈希值永远不会改变 -> 所以我可以缓存这个哈希值。”
但现在我又在想:其实你可以对任何可哈希的对象这么说。(除了那些哈希值就是它们 ID 的对象。)
那么,除了那些哈希计算非常快的小对象,是否还有理由不缓存一个对象的哈希值呢?
3 个回答
通常情况下,Python中的大多数对象都是可变的,也就是说它们的内容可以被改变。所以如果一个对象的哈希值是根据它的属性计算出来的,一旦你改变了这些属性,哈希值也会随之改变。如果你的类确实是不可变的,并且所有参与计算哈希值的属性也是不可变的,那么你就可以把哈希值存起来,避免每次都重新计算。
之前提到过,在某些情况下,缓存哈希值可能是个不错的选择。需要注意的是,你不能直接使用@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)
。
我花了一些时间才弄明白为什么会出现这个错误,我想那些被引导到这里的人可能也会对此感兴趣。
当然,缓存哈希值是可以的。实际上,Python 对字符串本身就会这样做。这里的权衡是哈希计算的速度和保存哈希值所占的空间之间的关系。比如说,这就是为什么元组不缓存它们的哈希值,而字符串会缓存的原因(可以参考 增强请求 #1462796)。