为什么我不能在Python中使用列表作为字典键?


100

对于什么可以/不能用作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

因此,元组是一个不可变的类型,但是如果我在其中隐藏一个列表,那么它就不能成为键。.我不能像在模块内部一样轻松地隐藏一个列表吗?

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


1
这是一个很好的讨论: stackoverflow.com/questions/2671211/…–
Hernan

49
笑出您的变量名。
kindall 2011年

Answers:


33

Python Wiki中有一篇关于该主题的好文章:为什么列表不能成为字典键。如此处所述:

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

可以在不真正破坏任何要求的情况下完成此操作,但是会导致意外的行为。通常将列表视为其值是从其内容的值派生的,例如在检查(不等式)时。可以理解的是,许多人希望您可以使用任何列表[1, 2]来获取相同的键,而您必须在其中保留完全相同的列表对象。但是,一旦修改了用作键的列表,按值查找就会中断,并且要通过标识查找,您需要保持完全相同的列表-这不需要任何其他常见的列表操作(至少我不能想到) )。

object无论如何,其他对象(例如模块)都会通过它们的对象标识产生更大的影响(这是您最后一次有两个不同的名为sys?的模块对象),并且无论如何都要进行比较。因此,当它们用作dict键时,在这种情况下也按标识进行比较就不足为奇了-甚至没有想到。


30

为什么我不能在Python中使用列表作为字典键?

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

(对于任何偶然发现此问题以寻求解决方案的人)

正如这里其他人所解释的,实际上您不能。但是,如果您确实要使用列表,则可以使用其字符串表示形式。


5
抱歉,我看不出你的意思。与使用字符串文字作为键没有什么不同。
2011年

11
真正; 我只是看到了这么多答案,实际上是在解释您为什么不能使用“键必须是可散列的”列表来使用列表,这是真的,我想提出一种解决方法,以防万一有人(新手)在寻找它...
雷米(Remi)

5
为什么不将列表转换为元组呢?为什么将其转换为字符串?如果您使用元组,它将与具有自定义比较方法的类一起正常使用__eq__。但是,如果将它们转换为字符串,则将通过字符串表示形式对所有内容进行比较。
阿兰·菲

好点@ Aran-Fey。只要确保元组中的任何元素本身都是可哈希的即可。例如,以元组([[1,2 ,, [2,3]])作为键将不起作用,因为元组的元素仍然是列表。
雷米

17

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

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

15

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

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

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

最终,没有令人满意的答案。例如,如果唯一起作用的键是原始键,那么如果您没有对该键的引用,则无法再访问该值。使用其他所有允许的密钥,您可以构造一个密钥,而无需参考原始密钥。

如果我的两个建议都起作用,那么您将拥有非常不同的键,它们返回相同的值,这有点令人惊讶。如果仅原始内容有效,则您的密钥将很快失效,因为已修改了列表。


是的,它是相同的列表,因此我希望d[li]保留为5。 d[[1,2,3]]将引用另一个列表对象作为键,因此它将是KeyError。我真的没有看到任何问题..除了让密钥被垃圾回收可能使某些dict值不可访问。但是,这是一个现实的问题不是一个逻辑问题..
WIM

@wim:d[list(li)]出现KeyError 是问题的一部分。在几乎所有其他用例中li将与内容相同的新列表无法区分。它有效,但是对许多人来说却违反直觉。另外,您上一次真正需要使用列表作为dict键是什么时候?唯一的使用情况下,我可以想像是当你哈希一切由身份无论如何,在这种情况下,你应该做的,而不是依靠__hash____eq__被认同为基础的。

@delnan 问题仅仅是因为这样的复杂性,它不是很有用的命令吗?还是有某些原因导致它实际上可能会破坏命令?
威姆

1
@wim:后者。如我的回答所述,它并没有真正打破对dict键的要求,但可能会带来更多无法解决的问题。

1
@delnan-您的意思是说“前者”
Jason

9

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

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

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

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


3

您的遮阳篷可以在这里找到:

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

Python的新手常常想知道为什么,尽管语言既包含元组又包含列表类型,但是元组可用作字典键,而列表却不可用。这是一个经过深思熟虑的设计决定,可以通过首先了解Python词典的工作方式来最好地解释。

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


3

因为列表是可变的,所以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键,因为元组是不可变且可哈希的。


>>> x =(1,2,3321321321321,)>>> id(x)139936535758888 >>> z =(1,2,1321321321321,)>>> id(z)139936535760544 >>> id((1, 2,3321321321321,))139936535810768这3个元组的值相同,但id不同。因此,具有键x的字典对键z没有任何值吗?
Ashwani

@Ashwani,您尝试过吗?
timgeb

是的,它按预期工作,我的疑问是所有具有相同值的元组具有不同的ID。那么,该哈希是基于什么计算的呢?
Ashwani

@Ashwani的哈希xz是一样的。如果尚不清楚,请打开一个新问题。
timgeb

1
@Ashwani hash(x)hash(z)
timgeb

1

您问题的简单答案是,类列表未实现方法散列,该散列对于任何希望用作字典中键的对象都是必需的。但是散列的原因不相同方式实现它在说,元组类(基于容器的内容)是因为列表是可变的,以便编辑列表将需要散列重新计算,这可能意味着在列表中现在位于基础哈希表中的错误存储桶中。请注意,由于您无法修改元组(不可变的),因此不会遇到此问题。

附带说明,dictobjects查找的实际实现基于Knuth Vol。的算法D。3秒 6.4。如果您有这本书,那么可能值得一读,此外,如果您真的非常有兴趣,则可以在这里查看开发人员对dictobject实际实现的评论。它详细介绍了它的工作原理。您可能也对感兴趣的字典的实现有一个python讲座。它们遍历了键的定义以及前几分钟的哈希值。


-1

根据Python 2.7.2文档:

如果对象的哈希值在其生命周期内不发生变化(需要使用hash()方法),并且可以与其他对象进行比较(需要使用eq()或cmp()方法),则该对象是可哈希的。比较相等的可哈希对象必须具有相同的哈希值。

散列性使对象可用作字典键和set成员,因为这些数据结构在内部使用散列值。

Python的所有不可变内置对象都是可哈希的,而没有可变容器(例如列表或字典)是可哈希的。作为用户定义类实例的对象默认情况下是可哈希的;它们都比较不相等,并且其哈希值是其id()。

从不能添加,删除或替换其元素的意义上说,元组是不可变的,但是元素本身可能是可变的。列表的哈希值取决于其元素的哈希值,因此当您更改元素时它也会改变。

对列表散列使用id意味着所有列表的比较方式不同,这将令人惊讶且不便。


1
那没有回答问题,对吗?hash = id不会在第一段的末尾打破不变式,问题是为什么它没有那样做。

@delnan:我添加了最后一段进行澄清。
Nicola Musatti 2011年

-1

字典是一个HashMap,它存储您的键的映射,将值转换为哈希的新键以及值映射。

类似于(伪代码):

{key : val}  
hash(key) = val

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

任何可散列的内容(可以转换为散列,并保持静态值,即不可变,以形成如上所述的散列键)均符合条件,但是列表或集合对象可以随时随地变化,因此hash(key)也应只是为了与您的列表或集合同步而变化。

你可以试试 :

hash(<your key here>)

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


简而言之 :

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.