字典是在 Python 3.6+ 中排序的吗?

Posted

技术标签:

【中文标题】字典是在 Python 3.6+ 中排序的吗?【英文标题】:Are dictionaries ordered in Python 3.6+? 【发布时间】:2021-06-13 22:26:11 【问题描述】:

字典在 Python 3.6 中排序(至少在 CPython 实现下)与以前的版本不同。这似乎是一个重大变化,但它只是documentation 中的一小段。它被描述为 CPython 实现细节而不是语言特性,但也暗示这可能在未来成为标准。

在保留元素顺序的同时,新的字典实现如何比旧的实现更好?

这是文档中的文本:

dict() 现在使用“紧凑”表示 pioneered by PyPy。与 Python 3.5 相比,新 dict() 的内存使用量减少了 20% 到 25%。 PEP 468(在函数中保留 **kwargs 的顺序。)由此实现。这个新实现的顺序保留方面被认为是一个实现细节,不应依赖(这可能会在未来发生变化,但在更改语言规范之前,希望在几个版本中使用该语言的这个新 dict 实现为所有当前和未来的 Python 实现强制要求保持顺序的语义;这也有助于保持与随机迭代顺序仍然有效的旧版本语言的向后兼容性,例如 Python 3.5)。 (由 INADA Naoki 在issue 27350 提供。想法originally suggested by Raymond Hettinger。)

2017 年 12 月更新:对于 Python 3.7,dicts 保留插入顺序为 guaranteed

【问题讨论】:

在 Python-Dev 邮件列表中查看此线程:mail.python.org/pipermail/python-dev/2016-September/146327.html 如果您还没有看到它;它基本上是围绕这些主题进行的讨论。 如果现在应该对 kwargs 进行排序(这是个好主意)并且 kwargs 是 dict,而不是 OrderedDict,那么我想人们可以假设 dict 键将在 Python 的未来版本中保持有序,尽管文档另有说明。 @DmitriySintsov 不,不要做这样的假设。这是在编写 PEP 期间提出的一个问题,该 PEP 定义了 **kwargs 的顺序保留功能,因此使用的措辞是外交的:现在保证函数签名中的 **kwargs 是插入顺序-保留映射。他们使用术语 mapping 是为了不强制任何其他实现使 dict 有序(并在内部使用OrderedDict)并作为一种表示这不应该依赖的方式事实上,dict 没有被订购。 来自 Raymond Hettinger 的好 video explanation @wazoox,hashmap 的顺序和复杂性没有改变。该更改通过浪费更少的空间使哈希图更小,并且节省的空间(通常?)比辅助数组占用的空间多。更快、更小、更有序 - 您可以选择所有 3 个。 【参考方案1】:

字典是否在 Python 3.6+ 中排序?

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

从 Python 3.7 开始,这不再是实现细节,而是成为一种语言特性。 From a python-dev message by GvR:

做到这一点。 “字典保持插入顺序”是裁决。谢谢!

这仅仅意味着你可以依赖它。如果 Python 的其他实现希望成为 Python 3.7 的一致实现,它们还必须提供插入有序字典。


Python 3.6 字典实现如何在保持元素顺序的同时比旧的实现更好的[2]

基本上,通过保留两个数组

第一个数组dk_entries 按插入顺序保存字典的条目 (of type PyDictKeyEntry)。保留顺序是通过这是一个仅追加的数组来实现的,其中总是在末尾插入新项目(插入顺序)。

第二个dk_indices 保存dk_entries 数组的索引(即,指示dk_entries 中相应条目位置的值)。该数组充当哈希表。当一个键被散列时,它会导致存储在dk_indices 中的索引之一,并通过索引dk_entries 获取相应的条目。由于只保留索引,因此该数组的类型取决于字典的整体大小(范围从类型11 字节)到int32_t/int64_t4/8 字节)在32/64 位构建)

在之前的实现中,必须分配一个类型为PyDictKeyEntry、大小为dk_size 的稀疏数组;不幸的是,这也导致了很多空白空间,因为该数组不允许超过2/3 * dk_size 满for performance reasons。 (并且空白空间仍然PyDictKeyEntry的大小!)。

现在情况并非如此,因为仅存储 必需的 条目(已插入的条目)和 intX_t 类型的稀疏数组(X 取决于字典大小)@987654358 @s full 被保留。空白区域从PyDictKeyEntry 类型更改为intX_t

因此,显然,创建PyDictKeyEntry 类型的稀疏数组比用于存储ints 的稀疏数组需要更多的内存。

如果有兴趣,可以查看on Python-Dev关于此功能的完整对话,这是一个很好的阅读。


In the original proposal made by Raymond Hettinger,可以看到使用的数据结构的可视化,它抓住了这个想法的要点。

例如字典:

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

当前存储为[keyhash, key, value]:

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

相反,数据应按如下方式组织:

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

您现在可以直观地看到,在原始提案中,大量空间基本上是空的,以减少冲突并加快查找速度。使用新方法,您可以通过将稀疏性移动到索引中真正需要的位置来减少所需的内存。


[1]:我说“插入有序”而不是“有序”,因为 OrderedDict 的存在,“有序”暗示了 `dict` 对象*不提供*的进一步行为。 OrderedDicts 是可逆的,提供顺序敏感的方法,主要是提供顺序敏感的相等测试(`==`、`!=`)。 `dict`s 目前不提供任何这些行为/方法。
[2]:新的字典实现通过更紧凑的设计在**内存方面表现得更好**;这是这里的主要好处。速度方面,差异并没有那么大,有些地方新字典可能会引入轻微的回归(key-lookups, for example),而在其他地方(想到迭代和调整大小)应该会提高性能。 总体而言,字典的性能,尤其是在现实生活中,由于引入了紧凑性而有所提高。

【讨论】:

那么,当一个项目被删除时会发生什么? entries 列表是否已调整大小?还是保留一个空格?还是不时压缩? @njzk2 当一个项目被移除时,对应的索引被替换为DKIX_DUMMY,值为-2entry数组中的条目replaced by NULL,当执行插入时新值被附加到条目数组中,尚未能够辨别,但很确定何时索引填充超出2/3 阈值调整大小。如果存在许多 DUMMY 条目,这可能会导致缩小而不是增长。 @Chris_Rands 不,我看到的唯一实际回归是在message by Victor 中的跟踪器上。除了那个微基准之外,我没有看到其他问题/消息表明现实生活中的工作负载存在严重的速度差异。在某些地方,新 dict 可能会引入轻微的回归(例如键查找),而在其他地方(想到迭代和调整大小)会出现性能提升。 调整大小部分的更正:字典不会在您删除项目时调整大小,它们会在您重新插入时重新计算。因此,如果使用d = i:i for i in range(100) 创建一个dict,而您.pop 所有项目都没有插入,则大小不会改变。当您再次添加 d[1] = 1 时,将计算适当的大小并调整 dict 的大小。 @Chris_Rands 我很确定它会留下来。问题是,我之所以更改答案以删除关于“dict 被订购”的一揽子陈述,dicts 在OrderedDicts 的意义上不是订购的。值得注意的问题是平等。 dicts 对订单不敏感 ==OrderedDicts 对订单敏感。转储OrderedDicts 并将dicts 更改为现在具有顺序敏感的比较可能会导致旧代码出现大量损坏。我猜OrderedDicts 唯一可能改变的是它的实现。【参考方案2】:

下面是回答原来的第一个问题:

我应该在 Python 3.6 中使用 dict 还是 OrderedDict

我认为文档中的这句话实际上足以回答您的问题

这个新实现的顺序保留方面被认为是一个实现细节,不应依赖

dict 并不明确意味着是一个有序集合,所以如果你想保持一致并且不依赖于新实现的副作用,你应该坚持使用OrderedDict

让您的代码面向未来:)

关于 here 存在争议。

编辑:Python 3.7 将保留此功能 see

【讨论】:

似乎如果他们不意味着它是一个真正的功能,而只是一个实现细节,那么他们甚至不应该将它放入文档中。 我不确定您的编辑警告;由于保证仅适用于 Python 3.7,我假设 Python 3.6 的建议没有改变,即 dicts 在 CPython 中排序但不要指望它【参考方案3】:

更新: Guido van Rossum announced on the mailing list 从 Python 3.7 dicts 开始,在所有 Python 实现中都必须保留插入顺序。

【讨论】:

既然密钥排序是官方标准,那么 OrderedDict 的目的是什么?或者,它现在是多余的吗? 我猜 OrderedDict 不会是多余的,因为它有 move_to_end 方法并且它的相等性是顺序敏感的:docs.python.org/3/library/…。请参阅有关 Jim Fasarakis Hilliard 答案的注释。 @JonnyWaffles 看到 Jim 的回答和这个 Q&A ***.com/questions/50872498/… 如果您希望您的代码在 2.7 和 3.6/3.7+ 上运行相同,则需要使用 OrderedDict 可能很快就会有一个“UnorderedDict”供那些出于安全原因喜欢麻烦他们的字典的人;p【参考方案4】:

我想添加到上面的讨论中,但没有评论的声誉。

Python 3.8 在字典中包含 reversed() 函数(删除了与 OrderedDict 的另一个区别。

现在可以使用 reversed() 以反向插入顺序迭代字典和字典视图。 (由 Rémi Lapeyre 在 bpo-33462 中贡献。) See what's new in python 3.8

我没有看到任何提到相等运算符或OrderedDict 的其他功能,所以它们仍然不完全相同。

【讨论】:

【参考方案5】:

为了在2020年全面回答这个问题,我引用official Python docs的几句话:

在 3.7 版更改:保证字典顺序为插入顺序。这种行为是 CPython 3.6 的实现细节。

3.7 版更改:保证字典顺序为插入顺序。

3.8 版更改:字典现在是可逆的。

字典和字典视图是可逆的。

关于 OrderedDict 与 Dict 的 statement:

有序字典与普通字典一样,但具有一些与排序操作相关的额外功能。现在它们变得不那么重要了,因为内置的 dict 类获得了记住插入顺序的能力(这种新行为在 Python 3.7 中得到了保证)。

【讨论】:

以上是关于字典是在 Python 3.6+ 中排序的吗?的主要内容,如果未能解决你的问题,请参考以下文章

在 Python 3.6+ 中有效地按位置访问字典项

Python 3.6 和 Python 3.5 中字典顺序之间的差异 [重复]

将列表转换为 DataFrame 并在 DataFrame 列中拆分嵌套字典 - Python 3.6

python 3.6与旧版本中的字典顺序

如何将字典的格式更改为 value1;key1;在 Python 3.6 中?

将嵌套字典转换为表/父子结构,Python 3.6