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

Posted

技术标签:

【中文标题】为啥我不能在 python 中使用列表作为 dict 键?【英文标题】:Why can't I use a list as a dict key in python?为什么我不能在 python 中使用列表作为 dict 键? 【发布时间】:2011-11-07 15:01:47 【问题描述】:

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

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

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

【问题讨论】:

这是一个很好的讨论:***.com/questions/2671211/… 从你的变量名中笑了出来。 【参考方案1】:

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

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

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

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

【讨论】:

【参考方案2】:

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

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: d[list(li)] 是 KeyError 是问题的一部分。在几乎所有其他用例中,li 与具有相同内容的新列表无法区分。它有效,但对许多人来说是违反直觉的。另外,你上一次真的必须使用列表作为字典键是什么时候?我能想象的唯一用例是当你无论如何都要通过身份对所有内容进行哈希处理时,在这种情况下你应该这样做,而不是依赖__hash____eq__ 来基于身份。 @delnan problem 仅仅是因为这样的复杂性而不是很有用的 dict 吗?还是有什么理由让它实际上可以破坏一个字典? @wim:后者。正如我在回答中所说,它并没有真正打破对 dict 键的要求,但它可能会引入比它解决的问题更多的问题。 @delnan - 你的意思是说“前者”【参考方案3】:

Python wiki 中有一篇关于该主题的好文章:Why Lists Can't Be Dictionary Keys。如那里所述:

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

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

其他对象,例如模块和object,无论如何都会从它们的对象标识中产生更大的影响(你上一次有两个不同的模块对象称为sys 是什么时候?),无论如何都要进行比较。因此,当它们用作 dict 键时,在这种情况下也会按身份进行比较,这并不令人惊讶 - 甚至是预期的。

【讨论】:

【参考方案4】:

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

为什么列表不能是字典键

Python 的新手常常想知道为什么,而该语言同时包含这两种语言 元组和列表类型,元组可用作字典键,而 列表不是。这是一个深思熟虑的设计决定,最好是 首先了解 Python 字典的工作原理。

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

【讨论】:

【参考方案5】:

根据 Python 2.7.2 文档:

一个对象是可哈希的,如果它有一个永远不会改变的哈希值 在其生命周期内(它需要一个__hash__() 方法),并且可以 与其他对象相比(它需要__eq__()__cmp__() 方法)。 比较相等的可散列对象必须具有相同的散列值。

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

所有 Python 的不可变内置对象都是可散列的,而没有 可变容器(例如列表或字典)是。对象 默认情况下,用户定义类的实例是可散列的;他们 都比较不等,它们的hash值就是它们的id()

元组是不可变的,你不能添加、删除或替换它的元素,但元素本身可能是可变的。 List 的 hash 值取决于它的元素的 hash 值,所以当你改变元素时它会改变。

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

【讨论】:

这不能回答问题,是吗? hash = id 没有破坏第一段末尾的不变量,问题是为什么它没有那样做。 @delnan:我添加了最后一段来澄清。【参考方案6】:

对您的问题的简单回答是,类列表没有实现任何希望用作字典中键的对象所需的方法 hash。然而,为什么 hash 没有像元组类(基于容器的内容)那样实现的原因是因为列表是可变的,所以编辑列表需要哈希重新计算,这可能意味着列表现在位于下级哈希表中的错误存储桶中。请注意,由于您无法修改元组(不可变),因此不会遇到此问题。

附带说明一下,dictobjects 查找的实际实现是基于 Knuth Vol. 中的算法 D。 3,秒。 6.4.如果您有这本书,那可能值得一读,此外,如果您真的非常感兴趣,您可能想看看开发人员 cmets 在实际 implementation of dictobject here. 上的详细信息这个怎么运作。还有一个python lecture 介绍您可能感兴趣的字典的实现。它们会在最初几分钟内完成键的定义以及哈希是什么。

【讨论】:

【参考方案7】:

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

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

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

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

【讨论】:

对不起,我真的不明白你的意思。使用字符串字面量作为键没有什么不同。 真;我刚刚看到这么多答案实际上解释了为什么你不能使用'key must be hashable'方面的列表,这是真的,我想提出一种解决方法,以防万一有人(新)会寻找它... 为什么不把列表转换成元组呢?为什么要转换成字符串?如果您使用元组,它将与具有自定义比较方法__eq__ 的类一起正常工作。但是如果你把它们转换成字符串,所有的东西都会通过它的字符串表示来比较。 好点@Aran-Fey。只需确保元组中的任何元素本身都是可散列的。例如tuple([[1,2],[2,3]]) 作为键不起作用,因为元组的元素仍然是列表。【参考方案8】:

刚刚发现可以把List变成tuple,然后作为ke​​y使用。

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

【讨论】:

【参考方案9】:

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

【讨论】:

>>> x=(1,2,3321321321321,) >>> id(x) 139936535758888 >>> z=(1,2,3321321321321,) >>> id(z) 139936535760544 >>> id((1,2,3321321321321,)) 139936535810768 这三个具有相同的元组值但不同的id。那么键为 x 的字典对键 z 没有任何价值? @Ashwani 你试过了吗? 是的,它按预期工作,我怀疑所有具有相同值的元组都有不同的 id。那么这个哈希值是根据什么计算出来的呢? @Ashwani xz 的哈希值相同。如果有什么不清楚的地方,请提出一个新问题。 @Ashwani hash(x)hash(z)【参考方案10】:

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

类似(伪代码):

key : val  
hash(key) = val

如果您想知道哪些可用选项可用作字典的键。那么

任何可散列的东西(可以转换为散列,并保持静态值,即不可变,以便生成 如上所述的散列键) 是合格的,但因为列表或集合对象可以随时变化,所以 hash(key) 也应该变化,以便与您的列表或集合保持同步。强>

你可以试试:

hash(<your key here>)

如果它工作正常,它可以用作字典的键,或者将其转换为可散列的东西。


简而言之:

    将该列表转换为tuple(&lt;your list&gt;)。 将该列表转换为str(&lt;your list&gt;)

【讨论】:

【参考方案11】:

我们只需记住dict 键需要不可变(准确地说,可散列)。列表是可变的(确切地说,列表不提供有效的__hash__ 方法)。

这里的不可变对象(unchangeable object)是一个对象,其状态在创建后就无法修改。这与可变对象(changeable object)形成对比,可变对象创建后可以修改。

【讨论】:

以上是关于为啥我不能在 python 中使用列表作为 dict 键?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我不能使用 folium.Map() 函数在地图中标记值列表?

为啥我不能使用模型方法作为默认值?无论如何,Python/Django 模型中的 self 是啥?

为啥 python 不能矢量化 map() 或列表推导

python dic字典使用

以字典作为可选参数的函数 - Python

为啥这个参数列表在 Python 中不会改变?