OrderedDict 如何知道已经实例化的字典的元素顺序?

How can OrderedDict know about the element order of an already instantiated dict?

我在玩 Python 3.6 中的 OrderedDict 类型,对其行为感到惊讶。当我在 IPython:

中创建一个像这样的简单 dict
d = dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])

我得到:

{'guido': 4127, 'jack': 4098, 'sape': 4139}

作为输出,出于某种原因,它在实例化时不保留元素的顺序。现在,当我像这样从 d 创建一个 OrderedDict 时:

od = OrderedDict(d)

输出为:

OrderedDict([('sape', 4139), ('guido', 4127), ('jack', 4098)])

现在我问自己,OrderedDict-构造函数如何知道 d 实例化时元素的顺序?它的行为是否始终相同,这样我就可以依赖 OrderedDict?

中元素的顺序

我已经在阅读有关字典和 OrderedDict 的 Python 文档,但我没有找到问题的答案。

(sys.version) 的输出:

In[22]: sys.version
Out[22]: '3.6.1 (default, Apr  4 2017, 09:40:21) \n[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.38)]'

很明显自定义挂钩 (sys.displayhook) that IPython uses to display output is pretty printing things (using it's own pretty printer)。

通过直接调用displayhook你可以看到它是如何破坏插入顺序的:

In [1]: from sys import displayhook
   ...: displayhook({'1': 0, '0': 1})
Out[1]: {'0': 1, '1': 0}

此外,如果您改为获取字典 str(发送要显示的字符串而不是字典对象),您将获得正确的预期顺序:

In [2]: d = dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
   ...: d
Out[2]: {'guido': 4127, 'jack': 4098, 'sape': 4139}

In [3]: str(dict(t))
Out[3]: "{'sape': 4139, 'guido': 4127, 'jack': 4098}"

类似地 printing 它。

我不确定为什么 IPython 使用 3.6 这样做,这很令人困惑(编辑:参见相关 issue on GitHub)。在您的标准 Python REPL 中,此行为不会出现,因为 sys.displayhook 未实现任何漂亮的打印。


您创建的字典 d 确实 保持插入顺序,这就是 OrderedDict 保持相同顺序的原因。

当然,它确实是一个实现细节。在这种情况发生改变之前(而且看起来确实会发生),您应该坚持使用 OrderedDict 来可靠地维护跨实现的顺序。


顺便说一下,如果你想禁用它,你可以使用 --no-pprint 选项启动 IPython 来禁用它的漂亮打印机:

➜ ipython --no-banner --no-pprint 

In [1]: dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
Out[1]: {'sape': 4139, 'guido': 4127, 'jack': 4098}

您可能知道,Python 中的词典不是根据语言规范排序的。它们确实有一个固有的顺序,但这个顺序是任意的。

因此,当您将标准字典传递给 OrderedDict 的构造函数时,新的 OrderedDict 将通过迭代其值从原始字典的值中填充。这样,将使用字典的固有顺序,这就是您将在最终 OrderedDict.

中看到的内容

现在,在 Python 3.6 中,默认字典的实现发生了变化。正如在 上讨论和解释的那样,标准词典现在保留插入顺序。这就是为什么当您从 Python 3.6 字典创建 OrderedDict 时,原始顺序也会被保留。

这是否意味着 OrderedDict 在 Python 3.6+ 中已过时?不,因为标准词典的顺序保留是一个 实现细节 。与以前实现的任意顺序不同,新字典恰好具有“正确”的顺序。但这绝不是语言规范所保证的,其他实现可能是这样,也可能不是。因此,您不能也不应该依赖它。

顺便说一句。请注意,Python 3.6(语言,而不仅仅是实现)确实保证保留 OrderedDict 的关键字参数的顺序。例如。这保留了顺序:

>>> OrderedDict(sape=4139, guido=4127, jack=4098)
OrderedDict([('sape', 4139), ('guido', 4127), ('jack', 4098)])

在 3.6 中,作为实现细节,所有 dict 都是有序的。你被IPython愚弄了:在3.6之前,键的顺序是任意的,所以为了用户友好,IPython对dictset的交互输出(其中正常 Python 只会打印 repr) 对键进行排序。这就是为什么您的 dict 看起来是按字母顺序排列的。当 运行 在 3.6+ 上时,IPython 可能最终会放弃该行为,因为正如您所注意到的,它非常令人困惑。

如果你显式 print,而不是依赖 ipython 为你输出前面表达式的结果,你将绕过 ipython 的 REPL 魔法并看到"natural"订单。与 dict 进行交互的任何其他方式也是如此,因为迭代将按预期顺序进行。