Python 3.6+中的字典是有序的吗?

740

从Python 3.6开始,字典是按插入顺序排序的。这被描述为CPython实现细节,而不是语言特性。文档中指出:

dict()现在采用了PyPy所开创的“紧凑”表示法。与Python 3.5相比,新的dict()的内存使用率降低了20%至25%。PEP 468(保留函数中**kwargs的顺序)也是通过这种方式实现的。这种新实现的有序性被认为是一种实现细节,不应依赖它(这可能会在未来改变,但希望在更改语言规范以要求所有当前和未来的Python实现都具有有序语义之前,在语言中保留这种新的dict实现数个版本;这也有助于保持向后兼容性,以便与仍然存在随机迭代顺序的旧版本语言(例如Python 3.5)保持兼容)。 (由INADA Naoki在issue 27350中提供。最初的想法由Raymond Hettinger建议。)

新的字典实现在保留元素顺序的同时,性能比旧实现更好。

2017年12月更新:Python 3.7 保证 dict 保留插入顺序。


5
如果你还没有看过它,可以查看Python-Dev邮件列表上的这个主题:https://mail.python.org/pipermail/python-dev/2016-September/146327.html,它基本上是围绕这些主题展开的讨论。请注意,此处只需要翻译,不会提供解释或其他信息。 - mgc
2
如果kwargs现在应该是有序的(这是个好主意),而kwargs是字典,不是OrderedDict,那么我猜想在未来版本的Python中,字典键将保持有序,尽管文档说得不一样。 - Dmitriy Sintsov
6
不要做那个假设。这是在撰写定义**kwargs保留顺序特性的PEP时提出的问题,因此所使用的措辞是委婉的:函数签名中的**kwargs现在保证是一个保留插入顺序的“映射”。他们使用了术语“映射”,以避免强制其他实现使字典有序(并在内部使用OrderedDict),并作为一种信号表明这不应该依赖于字典没有排序这一事实。 - Dimitris Fasarakis Hilliard
13
Raymond Hettinger的这个视频讲解很好。 - Alex
1
@wazoox,哈希映射的排序和复杂度没有改变。这个改变使哈希映射更小,浪费的空间更少,而且节省下来的空间(通常?)比辅助数组所占用的空间还要多。更快、更小、有序——你可以选择三者兼得。 - John La Rooy
显示剩余8条评论
6个回答

825

Python 3.6+版本中的字典是按插入顺序排序[1]

对于Python的CPython实现,从Python 3.6开始,字典会记住插入的顺序。然而,在Python 3.6中,这被认为是一个实现细节;如果你想要在其他Python实现中(以及其它有序行为[1])保证插入顺序,你需要使用OrderedDict

从Python 3.7开始,这是一项保证的语言特性,不再仅仅是一个实现细节。GvR在python-dev邮件中的声明

让它变成现实吧。“字典保持插入顺序”就是规则。谢谢!

这意味着你可以依赖这一点。如果其他Python实现希望成为Python 3.7的符合实现,它们也必须提供一个插入顺序的字典。


如何在保留元素顺序的同时,Python 3.6字典实现比旧版更高效呢?本质上是通过“保持两个数组”的方式。
  • 第一个数组 dk_entries 保存了字典中按插入顺序排列的条目 (类型为 PyDictKeyEntry)。为了保持顺序,这是一个只追加的数组,新项目总是在末尾插入(插入顺序)。

  • 第二个数组 dk_indices 保存了 dk_entries 数组的索引(即指示相应条目在 dk_entries 中的位置的值)。该数组作为哈希表。当键被哈希时,它会导致存储在 dk_indices 中的索引之一,并且通过对 dk_entries 进行索引来获取相应的条目。由于只保留索引,因此此数组的类型取决于字典的整体大小(在32位/64位构建上从类型 int8_t1字节)到 int32_t/int64_t4/8字节)不等)。

在之前的实现中,需要分配一个大小为dk_sizePyDictKeyEntry稀疏数组;不幸的是,由于该数组不允许超过2/3 * dk_size的空间被使用出于性能原因,这也导致了大量的空白空间(而且空白空间仍然具有PyDictKeyEntry大小)。
现在情况已经不同了,因为只存储了必要的条目(已插入的条目),并保留了一种名为intX_tX取决于字典大小)的稀疏数组,其空间使用率为2/3 * dk_size。空白空间从PyDictKeyEntry类型变成了intX_t类型。
显然,创建PyDictKeyEntry类型的稀疏数组比用于存储int的稀疏数组更加占用内存。

如果您感兴趣,可以查看Python-Dev上关于此功能的完整对话,这是一个不错的阅读材料。


在 Raymond Hettinger 最初的提案中,可以看到使用的数据结构的可视化,这捕捉了这个想法的要点。

For example, the dictionary:

d = {'timmy': 'red', 'barry': 'green', 'guido': 'blue'}

is currently stored as [keyhash, key, value]:

entries = [['--', '--', '--'],
           [-8522787127447073495, 'barry', 'green'],
           ['--', '--', '--'],
           ['--', '--', '--'],
           ['--', '--', '--'],
           [-9092791511155847987, 'timmy', 'red'],
           ['--', '--', '--'],
           [-6480567542315338377, 'guido', 'blue']]

Instead, the data should be organized as follows:

indices =  [None, 1, None, None, None, 0, None, 2]
entries =  [[-9092791511155847987, 'timmy', 'red'],
            [-8522787127447073495, 'barry', 'green'],
            [-6480567542315338377, 'guido', 'blue']]

如您现在可以看到的那样,在原始提案中,很多空间基本上是空着的,以减少冲突并使查找更快。采用新方法,您可以通过将稀疏性移到索引所需的位置来减少所需的内存。


[1]: 我使用“插入顺序”而不是“有序”,因为存在OrderedDict,使用“有序”会暗示`dict`对象提供了进一步的行为,但实际上没有。OrderedDict是可逆的,提供了顺序敏感的方法,主要提供了顺序敏感的相等测试(`==`, `!=`)。目前,`dict`不提供任何这些行为/方法。
[2]: 新的字典实现在内存方面表现更好,因为它被设计得更紧凑;这是主要的优点。就速度而言,并没有太大的差异,在某些情况下,新字典可能会引入轻微的退步(key-lookups,例如),而在其他情况下(比如迭代和调整大小),应该会有性能提升。
总体而言,由于引入了紧凑性,字典的性能,特别是在实际情况下,得到了改善。

29
当一个项目被移除时会发生什么?entries列表会被调整大小吗?还是保留空白位置?或者定期进行压缩? - njzk2
32
当一个项被移除时,相应的索引会被替换为值为-2DKIX_DUMMY,在entry数组中的条目也会被替换为NULL。当执行插入操作时,新值将追加到entries数组中。还没有确定,但很确定当索引填满超过2/3的阈值时,会执行重新调整大小操作。如果存在许多DUMMY条目,则可能会导致缩小而不是增长。 - Dimitris Fasarakis Hilliard
3
@Chris_Rands 没有,我看到的唯一一个实际的回归是在跟踪器中 Victor 的消息中。除了那个微基准测试之外,我没有看到其他任何问题/消息表明在实际工作负载中存在严重的速度差异。有些地方,新字典可能会引入轻微的回归(例如键查找),而在其他地方(例如迭代和调整大小)可能会有性能提升。 - Dimitris Fasarakis Hilliard
4
在大小调整部分的更正:在你删除项时,字典不会调整大小,而是在重新插入时重新计算。因此,如果使用d = {i:i for i in range(100)}创建一个字典,并且您使用.pop将所有项目弹出而未插入任何内容,则其大小不会改变。当您再次添加内容时,例如 d[1] = 1,适当的大小将被计算并且字典会调整大小。 - Dimitris Fasarakis Hilliard
9
我相信它将会保留。但是,我更改了我的答案以消除关于“dict有序”的概括性陈述,因为在dict的意义上,其不像OrderedDict那样是有序的。 显著的问题是相等性。 dict具有顺序不敏感的==,而OrderedDict则具有顺序敏感的==。将OrderedDict转储并将dict更改为现在具有顺序敏感比较可能会导致旧代码出现很多问题。 我猜想关于OrderedDict唯一可能发生变化的事情就是它的实现。 - Dimitris Fasarakis Hilliard
显示剩余18条评论

82
以下是对原始第一个问题的回答:
“在Python 3.6中,我应该使用dict还是OrderedDict?” 我认为文档中的这句话已经足够回答你的问题了:
“这个新实现中保留顺序的方面被认为是一个实现细节,不应该依赖它。”
dict并没有明确地意味着是一个有序集合,因此如果你想保持一致并且不依赖于新实现的副作用,你应该坚持使用OrderedDict。
让你的代码具备未来的可扩展性 :)
关于这个问题有一场辩论here
编辑: Python 3.7将保留这个特性see

36

更新: Guido van Rossum在邮件列表中宣布,从Python 3.7开始,所有Python实现中的dict都必须保留插入顺序。


3
现在键排序已成为官方标准,OrderedDict还有什么用途?或者说现在已经不再需要它了吗? - Jonny Waffles
6
我猜想OrderedDict不会变得多余,因为它有move_to_end方法并且其相等性是有序敏感的:https://docs.python.org/3/library/collections.html#collections.OrderedDict。请参见Jim Fasarakis Hilliard答案的说明。 - fjsj
@JonnyWaffles 请查看Jim的回答以及这个问题和答案 https://dev59.com/q1UL5IYBdhLWcg3wFErw - Chris_Rands
5
如果你希望你的代码在2.7和3.6/3.7+上运行结果相同,你需要使用OrderedDict。 - boatcoder
7
很可能很快会有一个"UnorderedDict",供那些喜欢为了安全原因而纠缠于他们的字典的人使用。; p - ZF007

23

我想要参与上面的讨论,但是没有足够的声望来评论。

Python 3.8 包括在字典上使用 reversed() 函数(从而消除了与OrderedDict的另一个差异)。

现在可以使用 reversed() 按反向插入顺序迭代字典和 dictviews。(由 Rémi Lapeyre 在 bpo-33462 中贡献。) 查看 Python 3.8 的新特性

我没有看到有关等号运算符或其他OrderedDict功能的提及,因此它们仍然不完全相同。


15
为了全面回答2020年的这个问题,让我引用来自Python官方文档的几个声明:
- 从3.7版本开始更改:字典顺序确保是插入顺序。这个行为在CPython 3.6中是一种实现细节。 - 从3.7版本开始更改:字典顺序确保是插入顺序。 - 从3.8版本开始更改:现在可以对字典进行反转。 - 字典和字典视图是可反转的。
关于OrderedDict与Dict的声明
- 有序字典与常规字典类似,但具有一些额外的有关排序操作的能力。 自从内置dict类获得记住插入顺序的能力后(这个新行为在Python 3.7中得到保证),它们变得不那么重要了。

2

从 3.7 版本开始:字典的顺序保证与插入顺序相同。这个行为是自 CPython 3.6 开始的一项实现细节。


你有没有阅读问题和其他答案?这些信息已经重复多次了。 - Chris_Rands
1
我投了+1票,因为这个答案提供了我所寻找的东西:一个单一、明确的参考。 - Jonathon Reinhart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接