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

Posted

技术标签:

【中文标题】在 Python 3.6+ 中有效地按位置访问字典项【英文标题】:Accessing dictionary items by position in Python 3.6+ efficiently 【发布时间】:2019-03-01 15:34:54 【问题描述】:

我知道字典是 insertion ordered in Python 3.6+,作为 3.6 中的实现细节和 3.7+ 中的官方。

鉴于它们是有序的,不存在按插入顺序检索字典的 ith 项的方法似乎很奇怪。可用的only solutions 似乎具有 O(n) 复杂度,或者:

    通过 O(n) 过程转换为列表,然后使用list.__getitem__enumerate 循环中的字典项,并在达到所需索引时返回值。同样,时间复杂度为 O(n)。

由于从list 获取项目具有 O(1) 复杂性,有没有办法用字典实现相同的复杂性?使用常规的dictcollections.OrderedDict 都可以。

如果不可能,是否存在阻止这种方法的结构性原因,或者这只是一个尚未考虑/实施的功能?

【问题讨论】:

它被实现为链表。否则,将无法删除元素。 我能想到一个晦涩难懂的原因。它使 JSON 行更稳定,无需封闭列表和单独的字典。除了那些非常小的细节之外,我还没有真正理解炒作 @o11c 根据***.com/a/39980744/987358,只有一个数组dk_entries按插入顺序排列的条目。没有链表。删除的条目被一个虚拟对象替换,当添加新项目时,可能会调整数组的大小(删除虚拟对象)。 @o11c 它实现为链表 我认为他们只是不想将类似序列的行为添加到基本上是映射的内容中。 IOW:您不应该像列表一样使用您的 dict,但它确实维持秩序。 【参考方案1】:

对于OrderedDict,它本质上是O(n),因为排序记录在linked list 中。

对于内置 dict,有一个向量(一个连续数组)而不是一个链表,但最后几乎是一样的:向量包含一些“假人”,特殊的内部值表示“不密钥已存储在这里”或“以前存储在这里但不再存在的密钥”。这使得,例如,删除密钥非常便宜(只需用虚拟值覆盖密钥)。

但如果不添加辅助数据结构,就无法跳过假人而不一次跳过它们。因为 Python 使用一种开放寻址形式来解决冲突,并将负载因子保持在 2/3 以下,所以至少有三分之一的向量条目 是虚拟的。 the_vector[i] 可以在 O(1) 时间访问,但实际上与第 i 个非虚拟条目没有可预测的关系。

【讨论】:

根据我对>3.6实现的理解,有两个向量,稀疏索引数组是发生开放寻址的地方,但实际的条目向量只是那,一个按顺序排列的条目数组,没有假人,不是吗? @juanpa.arrivillaga,它更复杂 - 什么不是? ;-) 封面下有“split”和“non-split”字典等。对于常规的旧字典(“non-split”),删除一个键设置相应的值槽为NULL,所以同样的事情;您必须一次跳过一个 NULL 值。请参阅 dictobject.c 的 dictiter_iternextkey() 中的循环。遍历“键”实际上 遍历值,这些值按插入顺序排列,但可以在任意位置包含 NULL。一旦找到非 NULL 值,它就会包含一个指向键的指针。 啊,我明白了。只是为了确保我正确理解您,当您删除一个键时,它实际上在条目向量中设置为null。这与 POC 实现 here 不同,后者的值只是从向量中弹出(__delitem__ 中的列表 self.entries)?我认为动机不是因为删除而导致 O(N) 惩罚? 您链接到的 POC 完全是为了其他目的:更节省空间的 dict 实现。它根本不保留插入顺序。实际上,它的“与最后一个条目交换以避免留下'洞'”可以将最后一个条目移动到任何位置。当前的实现既节省空间又保持顺序,但在删除时不留下任何漏洞,同时保持顺序需要物理移动被删除条目之后的每个条目。相反,它只是用 NULL 覆盖已删除的值(留下“一个洞”)。【参考方案2】:

根据@TimPeters' answer,您无法在 O(1) 时间内按位置访问字典项目是有结构性原因的。

如果您正在寻找通过键 位置进行 O(1) 查找,则值得考虑替代方案。有 3rd 方库,例如 NumPy / Pandas,它们提供了这样的功能,尤其是对于不需要指针的数字数组非常有效。

使用 Pandas,您可以构建具有独特标签的“类字典”系列,通过“标签”或位置提供 O(1) 查找。您牺牲的是删除标签时的性能,这会产生 O(n) 成本,很像 list

import pandas as pd

s = pd.Series(list(range(n)))

# O(n) item deletion
del s[i]
s.drop(i)
s.pop(i)

# O(1) lookup by label
s.loc[i]
s.at[i]
s.get(i)
s[i]

# O(1) lookup by position
s.iloc[i]
s.iat[i]

pd.Series 绝不是dict 的直接替代品。例如,如果系列主要用作映射,则不会防止重复键,并且会导致问题。但是,如果数据存储在连续的内存块中(如上例所示),您可能会看到显着的性能提升。

另见:

    What are the advantages of NumPy over regular Python lists?。 What is the performance impact of non-unique indexes in pandas? Pandas DataFrame search is linear time or constant time?

【讨论】:

不错。我在想满足 OP 要求的最简单的数据结构是什么。 @EricDuminil,确实如此。人们并不总是认为“熊猫系列!”在考虑dict 的替代方案时,但如果满足某些标准,那么它肯定是可行的。语法也通常是可比较的,例如s[i], s.get(i), del s[i], s.keys(), s.items().

以上是关于在 Python 3.6+ 中有效地按位置访问字典项的主要内容,如果未能解决你的问题,请参考以下文章

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

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

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

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

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

Python 核心技术与实战 --02 字典和集合