Python 为什么字典和集合中的顺序是任意的?

Python 为什么字典和集合中的顺序是任意的?,python,dictionary,set,python-internals,Python,Dictionary,Set,Python Internals,我不明白python中的字典或集合是如何按“任意”顺序循环的 我的意思是,它是一种编程语言,所以语言中的一切都必须是100%确定的,对吗?Python必须有某种算法来决定选择字典或集合的哪一部分,第一部分、第二部分等等 我错过了什么 注意:这个答案是在Python 3.6中更改dict类型之前编写的。这个答案中的大部分实现细节仍然适用,但是字典中键的列表顺序不再由散列值决定。set实现保持不变 顺序不是任意的,而是取决于字典或集合的插入和删除历史,以及特定的Python实现。对于这个答案的其余部

我不明白python中的字典或集合是如何按“任意”顺序循环的

我的意思是,它是一种编程语言,所以语言中的一切都必须是100%确定的,对吗?Python必须有某种算法来决定选择字典或集合的哪一部分,第一部分、第二部分等等

我错过了什么

注意:这个答案是在Python 3.6中更改
dict
类型之前编写的。这个答案中的大部分实现细节仍然适用,但是字典中键的列表顺序不再由散列值决定。set实现保持不变

顺序不是任意的,而是取决于字典或集合的插入和删除历史,以及特定的Python实现。对于这个答案的其余部分,对于“dictionary”,您也可以阅读“set”;集合被实现为只包含键而没有值的字典

对键进行散列,并将散列值分配给动态表中的插槽(它可以根据需要增长或收缩)。这个映射过程可能会导致冲突,这意味着一个密钥必须根据已经存在的内容在下一个插槽中开槽

列出插槽上的内容循环,因此键按当前在表中的顺序列出

以键
'foo'
'bar'
为例,假设表大小为8个插槽。在Python 2.7中,
hash('foo')
-4177197833195190597
hash('bar')
327024216814240868
。模8,这意味着这两个键在插槽3和插槽4中开槽,然后:

>>> hash('foo')
-4177197833195190597
>>> hash('foo') % 8
3
>>> hash('bar')
327024216814240868
>>> hash('bar') % 8
4
这将通知他们的上市顺序:

>>> {'bar': None, 'foo': None}
{'foo': None, 'bar': None}
除3和4之外的所有插槽都是空的,在表中循环首先列出插槽3,然后列出插槽4,因此
'foo'
列在
'bar'
之前

但是,
bar
baz
的散列值正好相隔8,因此映射到完全相同的插槽,
4

>>> hash('bar')
327024216814240868
>>> hash('baz')
327024216814240876
>>> hash('bar') % 8
4
>>> hash('baz') % 8
4
他们的顺序现在取决于先开槽的键;第二个键必须移动到下一个插槽:

>>> {'baz': None, 'bar': None}
{'bar': None, 'baz': None}
>>> {'bar': None, 'baz': None}
{'baz': None, 'bar': None}
这里的表顺序不同,因为一个键或另一个键是先开槽的

CPython(最常用的Python实现)使用的底层结构的技术名称是a,它使用开放寻址。如果您很好奇,并且对C理解得足够好,那么可以查看一下,以了解所有(有良好文档记录的)详细信息。您还可以观看关于CPython
dict
如何工作的文章,或者选择一份,其中包括一章关于Andrew Kuchling编写的实现

请注意,在Python 3.3中,还使用了随机散列种子,使散列冲突不可预测,以防止某些类型的拒绝服务(攻击者通过造成大量散列冲突使Python服务器无响应)。这意味着给定字典或集合的顺序也取决于当前Python调用的随机哈希种子

其他实现可以自由地为字典使用不同的结构,只要它们满足文档化的Python接口,但我相信到目前为止,所有实现都使用哈希表的变体

CPython 3.6引入了一个新的
dict
实现,它可以保持插入顺序,并且引导速度更快,内存效率更高。新的实现没有保留一个大型稀疏表,其中每行引用存储的哈希值以及键和值对象,而是添加了一个较小的哈希数组,该数组只引用单独的“密集”表中的索引(仅包含与实际键值对相同数量的行),正是密集表按顺序列出了包含的项。看。请注意,在Python3.6中,这被视为一个实现细节,Python语言没有指定其他实现必须保持顺序。这在Python3.7中发生了变化,在这里详细介绍了这一点;要使任何实现与Python 3.7或更高版本正确兼容,它必须复制这种顺序保留行为。明确地说:这个变化不适用于集合,因为集合已经有了一个“小”散列结构

Python2.7及更新版本还提供了一个,一个
dict
的子类,它添加了一个额外的数据结构来记录键顺序。以一定的速度和额外的内存为代价,这个类记住了你插入密钥的顺序;然后,列出键、值或项将按该顺序进行。它使用存储在附加字典中的双链接列表来高效地保持订单的最新状态。看
OrderedICT
对象还有其他优点,例如可重新订购

如果您想要一个有序的集合,您可以安装;它适用于Python2.5及以上版本。

“任意”与“非确定”不是一回事

他们所说的是,字典迭代顺序没有“在公共接口中”的有用属性。几乎可以肯定的是,迭代顺序的许多属性完全由当前实现字典迭代的代码决定,但作者并没有向您承诺它们是您可以使用的。这使他们可以在不同的Python版本之间(甚至只是在不同的操作条件下,或者在运行时完全随机)更自由地更改这些属性,而不用担心程序会崩溃


因此,如果您编写的程序完全依赖于字典顺序中的任何属性,那么您就是在“破坏”使用字典类型的契约,Python开发人员并没有承诺这将始终有效,即使在您测试它时它现在似乎有效。它基本上相当于依赖于C中的“未定义行为”。

这更多的是在它作为副本关闭之前的响应


其他的是
key_hash = ht->hash_func(key);
index = key_hash & (ht->num_buckets - 1);
>>> set([0,1919,2000,3,45,33,333,5])
set([0, 33, 3, 5, 45, 333, 2000, 1919])
33 & (ht->num_buckets - 1) = 1
'0b100001' & '0b111'= '0b1' # 1 the index of 33
'0b11101111111' & '0b111' = '0b111' # 7 the index of 1919
'0b101001101' & '0b111' = '0b101' # 5 the index of 333
>>> map(hash, (0, 1, 2, 3))
  [0, 1, 2, 3]
>>> map(hash, ("namea", "nameb", "namec", "named"))
  [-1658398457, -1658398460, -1658398459, -1658398462]
class int:
    def __hash__(self):
        value = self
        if value == -1:
            value = -2
        return value