为什么我不能在python中使用列表作为dict键?

时间:2011-08-31 13:28:03

标签: python list dictionary tuples hashable

我对可以/不能用作python dict的键有点困惑。

dicked = {}
dicked[None] = 'foo'     # None ok
dicked[(1,3)] = 'baz'    # tuple ok
import sys
dicked[sys] = 'bar'      # wow, even a module is ok !
dicked[(1,[3])] = 'qux'  # oops, not allowed

所以元组是一个不可变类型,但如果我在其中隐藏一个列表,那么它就不能成为一个键......我不能轻易地隐藏模块中的列表吗?

我有一些模糊的想法,关键是必须“可以”,但我只是承认自己对技术细节的无知;我不知道这里到底发生了什么。如果您尝试使用列表作为键,将哈希作为其内存位置,会出现什么问题?

11 个答案:

答案 0 :(得分:21)

在Python维基中有一篇关于这个主题的好文章:Why Lists Can't Be Dictionary Keys。正如那里所解释的那样:

  

如果您尝试将列表用作键,将哈希作为其内存位置,会出现什么问题?

可以在不破坏任何要求的情况下完成,但会导致意外行为。列表通常被视为其值来自其内容的值,例如在检查(in-)相等时。许多人 - 可以理解 - 期望您可以使用任何列表[1, 2]来获取相同的密钥,您必须保持完全相同的列表对象。但是,一旦用作密钥的列表被修改就会通过值中断进行查找,并且对于按身份查找,需要您保持完全相同的列表 - 这对于任何其他常见列表操作都是不需要的(至少我无法做到)想到)。

模块和object之类的其他对象无论如何都会从它们的对象标识中产生更大的影响(当你最后一次有两个不同的模块对象叫sys时?),并通过它们进行比较无论如何。因此,当用作字典键时,它们在这种情况下也可以通过身份进行比较,从而不那么令人惊讶 - 甚至预期 -

答案 1 :(得分:20)

为什么我不能在python中使用列表作为dict键?

>>> d = {repr([1,2,3]): 'value'}
{'[1, 2, 3]': 'value'}

(对于任何偶然发现这个问题寻找方法的人来说)

正如其他人在这里解释的那样,确实你不能。但是,如果你真的想使用你的列表,你可以使用它的字符串表示。

答案 2 :(得分:10)

问题是元组是不可变的,而列表则不是。请考虑以下

d = {}
li = [1,2,3]
d[li] = 5
li.append(4)

d[li]应该返回什么?是同一个名单吗? d[[1,2,3]]怎么样?它具有相同的值,但是不同的列表?

最终,没有令人满意的答案。例如,如果唯一有效的密钥是原始密钥,那么如果您没有引用该密钥,则永远不能再次访问该值。使用其他所有允许的密钥,您可以构建密钥而无需引用原始密钥。

如果我的两个建议都有效,那么你有非常不同的键返回相同的值,这有点令人惊讶。如果只有原始内容有效,那么你的密钥很快就会变坏,因为列表会被修改。

答案 3 :(得分:7)

这是一个答案http://wiki.python.org/moin/DictionaryKeys

  

如果您尝试将列表用作键,将哈希作为其内存位置,会出现什么问题?

查找具有相同内容的不同列表会产生不同的结果,即使比较具有相同内容的列表也会表明它们是等效的。

在字典查找中使用列表文字怎么样?

答案 4 :(得分:5)

发现您可以将List更改为元组,然后将其用作键。

d = {tuple([1,2,3]): 'value'}

答案 5 :(得分:2)

你的awnser可以在这里找到:

  

为什么列表不能成为字典键

     

Python的新手常常想知道为什么,而语言包括两者   元组和列表类型,元组可用作字典键,而   列表不是。这是一个刻意的设计决定,最好的   通过首先理解Python字典的工作原理来解释。

来源&更多信息:http://wiki.python.org/moin/DictionaryKeys

答案 6 :(得分:1)

你的问题的简单答案是类列表没有实现方法 hash ,这是任何希望用作字典中的键的对象所必需的。然而, hash 之所以没有实现的原因与它说的元组类(基于容器的内容)相同是因为列表是可变的,因此编辑列表需要哈希是重新计算,这可能意味着现在位于底层哈希表中的错误桶中的列表。请注意,由于您无法修改元组(不可变),因此不会遇到此问题。

作为旁注,dictobjects查找的实际实现基于Knuth Vol的算法D. 3,Sec。 6.4。如果你有这本书可供你阅读,那么如果你真的非常感兴趣,你可能想看一看开发人员对实际的评论implementation of dictobject here.它会非常详细。确切地说它是如何工作的。您可能感兴趣的字典实现还有一个python lecture。它们会在最初的几分钟内完成一个键的定义和一个哈希。

答案 7 :(得分:1)

因为列表是可变的,所以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

这也不是一个好主意,因为相等的对象应该相同地散列,以便可以在dictset中找到它们。

示例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键,因为元组是不可变且可哈希的。

答案 8 :(得分:0)

根据Python 2.7.2文档:

  

如果对象具有永不更改的哈希值,则该对象是可清除的   在它的生命周期中(它需要一个哈希()方法),并且可以   与其他对象相比(它需要 eq ()或 cmp ()方法)。   比较相等的Hashable对象必须具有相同的哈希值。

     

Hashability使对象可用作字典键和集合   member,因为这些数据结构在内部使用哈希值。

     

所有Python的不可变内置对象都是可清除的,而没有   可变容器(如列表或词典)是。对象   用户定义的类的实例默认是可以清除的;他们   所有比较不相等,它们的哈希值是它们的id()。

元组是不可变的,因为你不能添加,删除或替换它的元素,但元素本身可能是可变的。 List的哈希值取决于其元素的哈希值,因此当您更改元素时它会发生变化。

将id用于列表哈希意味着所有列表都会有不同的比较,这会令人感到惊讶和不便。

答案 9 :(得分:-1)

字典是一个HashMap,它存储键的映射,并转换为值 到散列的新键和值映射。

(伪代码)之类的东西

{key : val}  
hash(key) = val

如果您想知道哪些可用选项可以用作字典的键。然后

任何可哈希的内容(可以转换为哈希值,并保留静态值,即不可变,从而使 符合上述条件的散列键)是合格的,但由于列表或集合对象可以随时随地变化,因此hash(key)也应仅随同您的列表或集合而变化。 strong>

您可以尝试:

hash(<your key here>)

如果工作正常,则可以将其用作字典的键,也可以将其转换为可哈希的内容。


简而言之:

  1. 将该列表转换为tuple(<your list>)
  2. 将该列表转换为str(<your list>)

答案 10 :(得分:-1)

dict键必须是可哈希的。列表是可变的,它们不提供有效的 hash 方法。

相关问题