因为列表是可变的,所以dict
键(和set
成员)必须是可哈希的,并且对可变对象进行哈希处理是一个坏主意,因为哈希值应基于实例属性进行计算。
在这个答案中,我将给出一些具体的例子,希望在现有答案的基础上增加价值。每个洞察力也适用于数据set
结构的元素。
示例1:哈希可变对象,其中哈希值基于对象的可变特性。
>>> class stupidlist(list):
... def __hash__(self):
... return len(self)
...
>>> stupid = stupidlist([1, 2, 3])
>>> d = {stupid: 0}
>>> stupid.append(4)
>>> stupid
[1, 2, 3, 4]
>>> d
{[1, 2, 3, 4]: 0}
>>> 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)。
TL; DR:您可以将其tuple(yourlist)
用作dict
键,因为元组是不可变且可哈希的。