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个插槽。在Python2.7中,散列'foo'是-4177197833195190597,散列'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:

他们的顺序现在取决于先开槽的键;第二个键必须移动到下一个插槽:

>>> {'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中发生了变化,在这里详细介绍了这一点;要使任何实现与Python3.7或更高版本正确兼容,它必须复制这种保留顺序的行为。明确地说:这个变化不适用于集合,因为集合已经有了一个“小”散列结构

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


如果您想要一个有序的集合,您可以安装;它适用于Python 2.5及更高版本。

任意性与非确定性不是一回事

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


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

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

其他人是对的:不要依赖订单。甚至不要假装有一个

也就是说,有一件事你可以依靠:

listmyset==listmyset 也就是说,秩序是稳定的

理解为什么存在感知的秩序需要理解以下几点:

Python使用哈希集

CPython的哈希集如何存储在内存中以及

数字是如何散列的

从顶部开始:

散列集是一种以非常快的查找时间存储随机数据的方法

它有一个后备阵列:

C阵列;项目可能为空, 指向对象的指针,或 特殊虚拟对象 _ _ 4 _ _ 2 _ _ 6 我们将忽略特殊的虚拟对象,它的存在只是为了使移除更容易处理,因为我们不会从这些集合中移除

为了实现真正快速的查找,您需要使用一些魔法来计算对象的哈希值。唯一的规则是两个相等的对象具有相同的散列。但如果两个对象具有相同的散列,则它们可能不相等

然后,通过按数组长度取模来生成索引:

hash4%lenstorage=索引2 这使得访问元素的速度非常快

散列只是故事的大部分,因为hashn%lenstorage和hashm%lenstorage可能会产生相同的数字。在这种情况下,几种不同的策略可以尝试解决冲突。在做复杂的事情之前,CPython使用了9次线性探测,所以它会在槽的左侧寻找最多9个位置,然后再寻找其他位置

CPython的哈希集存储方式如下:

散列集的完整度不能超过2/3。如果有20个元素,并且备份数组的长度为30个元素,则备份存储区的大小将调整为更大。这是因为使用小型备份存储时会更频繁地发生冲突,而冲突会减慢一切

备份存储以4的幂调整大小,从8开始,但大型集合50k元素的大小以2的幂调整:8、32、128

因此,创建阵列时,备份存储的长度为8。当它已满5个元素并且您添加了一个元素时,它将短暂地包含6个元素。6 > ²⁄₃·8因此,这会触发调整大小,并且备份存储将四倍于大小32

最后,hashn只为数字返回n,但-1是特殊的

那么,让我们来看第一个:

v_集={88,11,1,33,21,3,7,55,37,8} lenv_集合为10,因此在添加所有项目后,备份存储至少为15+1。2的相关幂为32。因此,支持存储是:

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ 我们有

hash88%32=24 hash11%32=11 hash1%32=1 hash33%32=1 hash21%32=21 hash3%32=3 hash7%32=7 hash55%32=23 hash37%32=5 hash8%32=8 因此,插入以下内容:

__ 1 __ 3 __ 37 __ 7 8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __ 33← 也不能在1所在的位置; 要么1要么33必须移动 所以我们希望能收到这样的订单

{[1或33],3,37,7,8,11,21,55,88} 用1或33,这不是在其他地方开始。这将使用线性探测,因此我们将:

↓ __ 1 33 3 __ 37 __ 7 8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __ 或

↓ __ 33 1 3 __ 37 __ 7 8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __ 您可能认为33是被替换的,因为1已经存在,但由于在构建集合时发生的大小调整,实际情况并非如此。每次重建集合时,已添加的项都会有效地重新排序

现在你知道为什么了

{7,5,11,1,4,13,55,12,2,3,6,20,9,10} 可能是正常的。有14个元素,因此备份存储至少为21+1,即32:

__ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ 前13个插槽中的1到13个哈希。20号进入20号槽

__ 1 2 3 4 5 6 7 8 9 10 11 12 13 __ __ __ __ __ __ 20 __ __ __ __ __ __ __ __ __ __ __ 55进入插槽hash55%32,即23:

__ 1 2 3 4 5 6 7 8 9 10 11 12 13 __ __ __ __ __ __ 20 __ __ 55 __ __ __ __ __ __ __ __ 如果我们选择50,我们会 期待

__ 1 2 3 4 5 6 7 8 9 10 11 12 13 __ __ __ __ 50 __ 20 __ __ __ __ __ __ __ __ __ __ __ 你瞧:

{1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 20, 50} >>> {1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 50, 20} pop的实现非常简单:它遍历列表并弹出第一个列表


这就是所有的实现细节。这个问题的其他答案都写得很好。OP问我如何解释他们如何逃脱或为什么

Python文档说,由于Python字典实现了。正如他们所说

绑定返回的顺序可能是任意的

换句话说,计算机科学专业的学生不能假设关联数组是有序的。对于中的集合也是如此

集合元素的排列顺序是不相关的

集合是一种抽象数据类型,可以存储某些值,而不需要任何特定的顺序

使用哈希表实现字典是一个有趣的问题,因为就顺序而言,它与关联数组具有相同的属性。

Python用于存储字典,因此字典或其他使用哈希表的可编辑对象中没有顺序

但是关于散列对象中项目的索引,python根据以下代码计算索引:

因此,由于整数的散列值是整数本身*索引基于数字ht->num_bucket-1是一个常数,因此通过按位and计算的ht->num_bucket-1和数字本身*除散列值为-2的-1外,其他对象的散列值为-2

考虑使用哈希表的集合的以下示例:

>>> set([0,1919,2000,3,45,33,333,5])
set([0, 33, 3, 5, 45, 333, 2000, 1919])
对于33号,我们有:

33 & (ht->num_buckets - 1) = 1
实际上是:

'0b100001' & '0b111'= '0b1' # 1 the index of 33
注意在这种情况下,ht->num_bucket-1是8-1=7或0b111

1919年:

333人:

有关python哈希函数的更多详细信息,请阅读以下引用:

主要的微妙之处:大多数散列方案依赖于良好的散列 函数,在模拟随机性的意义上。Python没有:它是最重要的 字符串和整数的重要哈希函数通常非常规则 案例:

这不一定是坏事!相反,在尺寸为2**i的表格中 作为初始表索引的低阶i位非常快,并且 对于由连续整数范围索引的DICT,根本没有冲突。 当键是连续字符串时,情况大致相同。那么这个 在普通情况下,比随机行为更好,这是非常可取的

OTOH,当碰撞发生时,填充连续切片的趋势 哈希表使得良好的冲突解决策略至关重要。仅服用 哈希代码的最后一个位也很脆弱:例如,考虑
名单[我不认为其他Python实现可以以这样或那样的方式使用任何不是哈希表的东西,尽管现在有数十亿种不同的方式来实现哈希表,所以仍然有一些自由。事实上,字典使用uuu hash_uu和uu eq_u,而没有其他任何东西实际上是语言保证,而不是实现细节@delnan:我想知道你是否仍然可以使用BTree进行散列和相等性测试。无论如何,我当然不会排除这种可能性。:-这当然是正确的,我很高兴被证明是错误的w.r.t.可行性,但我看不出有任何方法可以在不需要更广泛合同的情况下击败哈希表。BTree的平均情况性能不会更好nce并没有提供更好的最坏情况,哈希冲突仍然意味着线性搜索。因此,您只能更好地抵抗许多哈希,例如Congrent mod tablesize,还有许多其他很好的方法来处理dictobject.c中使用的某些哈希,并且最终得到的比较远少于BTree找到正确子树所需的比较e、 @delnan:我完全同意;我最不想因为不允许其他实现选项而被责骂。请注意,字典迭代的一部分定义得很好:迭代给定字典的键、值或项的顺序都是相同的,只要其间没有对字典做任何更改。That意味着d.items本质上与zipd.keys、d.values相同。但是,如果将任何项添加到字典中,则所有下注都将关闭。如果需要调整哈希表的大小,则顺序可能会完全改变,尽管大多数情况下,您只会发现新项出现在序列中的任意位置。最新的PyPy版本2.5,用于Python 2.7是这样的。你基本上是对的,但它会更接近,并给出一个很好的提示,说明为什么说它是一个无序的实现,而不是一个assoc数组。
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