由于列表是可变的,所以字典键(和集合成员)需要是可哈希的。对于可变对象进行哈希是个坏主意,因为哈希值应该基于实例属性计算。
在这个答案中,我将提供一些具体的例子,希望在现有答案的基础上添加价值。每一个洞见也适用于集合数据结构的元素。
示例1: 对可变对象进行哈希,其中哈希值是基于对象可变特征的。
>>> class stupidlist(list):
... def __hash__(self):
... return len(self)
...
>>> stupid = stupidlist([1, 2, 3])
>>> d =
>>> stupid.append(4)
>>> stupid
[1, 2, 3, 4]
>>> d
>>> stupid in d
False
>>> stupid in d.keys()
False
>>> stupid in list(d.keys())
True
在对 stupid
进行更改后,字典中将无法再找到它,因为哈希值已经改变。只有在字典键列表上进行线性扫描才能找到 stupid
。
示例2: ... 但为什么不使用一个恒定的哈希值呢?
>>> class stupidlist2(list):
... def __hash__(self):
... return id(self)
...
>>> stupidA = stupidlist2([1, 2, 3])
>>> stupidB = stupidlist2([1, 2, 3])
>>>
>>> stupidA == stupidB
True
>>> stupidA in {stupidB: 0}
False
这也不是一个好主意,因为相等的对象应该有相同的哈希值,这样你才能在字典(dict
)或集合(set
)中找到它们。
示例3: ... 好吧,那所有实例的哈希值都相同怎么样?!
>>> class stupidlist3(list):
... def __hash__(self):
... return 1
...
>>> stupidC = stupidlist3([1, 2, 3])
>>> stupidD = stupidlist3([1, 2, 3])
>>> stupidE = stupidlist3([1, 2, 3, 4])
>>>
>>> stupidC in {stupidD: 0}
True
>>> stupidC in {stupidE: 0}
False
>>> d = {stupidC: 0}
>>> stupidC.append(5)
>>> stupidC in d
True
事情似乎按预期工作,但请考虑正在发生的事情:当您的类的所有实例产生相同的哈希值时,只要在
dict
中有两个以上的实例作为键或者存在于一个
set
中,就会出现哈希冲突。
使用
my_dict[key]
、
key in my_dict
(或者
item in my_set
)来查找正确的实例,需要执行与
stupidlist3
的实例数目相同的等式检查(在最坏情况下)。此时,字典的目的——O(1)查找——完全被击败。下面是一些定时(使用IPython进行)的示例3。
>>> lists_list = [[i] for i in range(1000)]
>>> stupidlists_set = {stupidlist3([i]) for i in range(1000)}
>>> tuples_set = {(i,) for i in range(1000)}
>>> l = [999]
>>> s = stupidlist3([999])
>>> t = (999,)
>>>
>>> %timeit l in lists_list
25.5 µs ± 442 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit s in stupidlists_set
38.5 µs ± 61.2 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
>>> %timeit t in tuples_set
77.6 ns ± 1.5 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
如您所见,在我们的
stupidlists_set
中进行成员测试甚至比整个
lists_list
的线性扫描还要慢,而在没有大量哈希碰撞的集合中,您可以期望有超快的查找时间(500倍因子)。
简而言之:您可以使用
tuple(yourlist)
作为
dict
键,因为元组是不可变和可哈希的。