Why is the order in dictionaries and sets arbitrary?
我不明白在python中如何以"任意"顺序循环字典或集合。
我的意思是,这是一种编程语言,所以语言中的所有内容都必须100%确定,对吗?python必须有某种算法来决定选择字典或集合的哪一部分、第一部分、第二部分等等。
我错过了什么?
顺序不是任意的,而是取决于字典或集合的插入和删除历史,以及特定的Python实现。对于这个答案的其余部分,对于"dictionary",您还可以读取"set";集合作为字典实现,只有键,没有值。
键被散列,散列值被分配给动态表中的槽(它可以根据需要增长或收缩)。这个映射过程可能会导致冲突,这意味着必须根据已经存在的内容在下一个槽中开槽。
列出内容在插槽上循环,因此键按它们当前在表中的排列顺序列出。
以键
1 2 3 4 5 6 7 8 | >>> hash('foo') -4177197833195190597 >>> hash('foo') % 8 3 >>> hash('bar') 327024216814240868 >>> hash('bar') % 8 4 |
这将通知他们的上市顺序:
1 2 | >>> {'bar': None, 'foo': None} {'foo': None, 'bar': None} |
除3和4之外的所有插槽都是空的,在表上循环首先列出插槽3,然后是插槽4,因此
但是,
1 2 3 4 5 6 7 8 | >>> hash('bar') 327024216814240868 >>> hash('baz') 327024216814240876 >>> hash('bar') % 8 4 >>> hash('baz') % 8 4 |
它们的顺序现在取决于哪个键是第一个槽;第二个键必须移动到下一个槽:
1 2 3 4 | >>> {'baz': None, 'bar': None} {'bar': None, 'baz': None} >>> {'bar': None, 'baz': None} {'baz': None, 'bar': None} |
表的顺序在这里有所不同,因为其中一个键或另一个键是先开槽的。
cpython(最常用的python实现)使用的底层结构的技术名称是散列表,它使用开放寻址。如果您很好奇,并且对C有足够的了解,请查看C实现中的所有(有良好文档记录的)细节。您还可以观看Brandon Rhodes关于CPython
请注意,从python 3.3开始,还使用了随机散列种子,使得散列冲突无法预测,以防止某些类型的拒绝服务(在这种情况下,攻击者会导致大量散列冲突,从而使python服务器失去响应)。这意味着给定字典的顺序也依赖于当前Python调用的随机散列种子。
其他实现可以自由地为字典使用不同的结构,只要它们满足文档中为它们提供的Python接口,但是我相信到目前为止所有实现都使用了哈希表的变体。
cpython 3.6引入了一种新的
python2.7和更新版本还提供了一个
如果您想要订购一套,可以安装
这更像是对python 3.41集的响应,在它作为副本关闭之前。
其他人是对的:不要依赖命令。不要假装有。
也就是说,有一件事你可以依靠:
1 | list(myset) == list(myset) |
也就是说,顺序是稳定的。
理解为什么有一个感知的秩序需要理解一些事情:
python使用散列集,
cpython的散列集如何存储在内存中,以及
数字如何散列
从顶部:
哈希集是一种存储随机数据的方法,查找时间非常快。
它有一个后备阵列:
1 2 3 4 | # A C array; items may be NULL, # a pointer to an object, or a # special dummy object _ _ 4 _ _ 2 _ _ 6 |
我们将忽略特殊的虚拟对象,它的存在只是为了使删除更容易处理,因为我们不会从这些集合中删除。
为了实现真正快速的查找,您需要做一些魔术来计算对象的散列值。唯一的规则是两个相等的对象具有相同的哈希值。(但如果两个对象具有相同的哈希值,则它们可能不相等。)
然后,通过按数组长度取模来生成索引:
1 | hash(4) % len(storage) = index 2 |
这使得访问元素变得非常快速。
散列只是故事的大部分,因为
cpython的散列集存储如下:
哈希集不能超过2/3满。如果有20个元素,并且支持数组的长度为30个元素,则支持存储将调整为更大的大小。这是因为与小的后备存储器的碰撞更频繁,而碰撞会减慢所有操作的速度。
备份存储从8开始以4的幂次调整大小,但以2的幂次调整大小的大型集(50K元素)除外:(8,32,128,…)。
所以当您创建一个数组时,后备存储的长度是8。当它是5满的并且您添加了一个元素时,它将简短地包含6个元素。
最后,
那么,让我们看看第一个:
1 | v_set = {88,11,1,33,21,3,7,55,37,8} |
1 | __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ |
我们有
1 2 3 4 5 6 7 8 9 10 | hash(88) % 32 = 24 hash(11) % 32 = 11 hash(1) % 32 = 1 hash(33) % 32 = 1 hash(21) % 32 = 21 hash(3) % 32 = 3 hash(7) % 32 = 7 hash(55) % 32 = 23 hash(37) % 32 = 5 hash(8) % 32 = 8 |
因此,这些插入为:
1 2 3 | __ 1 __ 3 __ 37 __ 7 8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __ 33 ← Can't also be where 1 is; either 1 or 33 has to move |
所以我们希望能订购
1 | {[1 or 33], 3, 37, 7, 8, 11, 21, 55, 88} |
1或33不是从其他地方开始的。这将使用线性探测,因此我们将有:
1 2 | ↓ __ 1 33 3 __ 37 __ 7 8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __ |
或
1 2 | ↓ __ 33 1 3 __ 37 __ 7 8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __ |
您可能会期望33被替换,因为1已经存在,但是由于在构建集合时发生的调整大小,事实并非如此。每次重新构建集合时,都会有效地重新排序已添加的项。
现在你知道为什么了
1 | {7,5,11,1,4,13,55,12,2,3,6,20,9,10} |
可能是正常的。共有14个元素,因此后备存储器至少为21+1,这意味着32:
1 | __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ |
前13个槽中有1到13个哈希。20进入插槽20。
1 | __ 1 2 3 4 5 6 7 8 9 10 11 12 13 __ __ __ __ __ __ 20 __ __ __ __ __ __ __ __ __ __ __ |
55进入槽
1 | __ 1 2 3 4 5 6 7 8 9 10 11 12 13 __ __ __ __ __ __ 20 __ __ 55 __ __ __ __ __ __ __ __ |
如果我们选择50,我们会期待
1 | __ 1 2 3 4 5 6 7 8 9 10 11 12 13 __ __ __ __ 50 __ 20 __ __ __ __ __ __ __ __ __ __ __ |
你看:
1 2 | {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} |
"武断"和"不确定"不是一回事。
他们所说的是,字典迭代顺序没有"在公共接口中"的有用属性。几乎可以肯定的是,当前实现字典迭代的代码完全决定了迭代顺序的许多属性,但是作者并没有向您承诺它们是您可以使用的。这使他们可以更自由地在Python版本之间更改这些属性(甚至在不同的操作条件下,或者在运行时完全随机更改),而不用担心程序会中断。
因此,如果您编写的程序依赖于所有字典顺序中的任何属性,那么您就是在"打破使用字典类型的契约",而Python开发人员并没有承诺这将始终有效,即使在您测试它时它现在似乎有效。它基本上相当于依赖C语言中的"未定义行为"。
这个问题的其他答案很好,写得很好。操作询问"如何",我解释为"他们如何摆脱"或"为什么"。
python文档说字典没有排序,因为python字典实现抽象数据类型关联数组。正如他们所说的
the order in which the bindings are returned may be arbitrary
换句话说,一个计算机科学学生不能假定一个关联数组是有序的。数学中的集合也是如此
the order in which the elements of a set are listed is irrelevant
计算机科学
a set is an abstract data type that can store certain values, without any particular order
使用哈希表实现字典是一个很有趣的实现细节,因为就顺序而言,它与关联数组具有相同的属性。
python使用哈希表存储字典,因此字典或其他使用哈希表的可ITerable对象中没有顺序。
但是对于散列对象中的项目索引,python根据
1 2 | key_hash = ht->hash_func(key); index = key_hash & (ht->num_buckets - 1); |
因此,由于整数的散列值是整数本身*索引基于数字(
考虑使用哈希表的
1 2 | >>> set([0,1919,2000,3,45,33,333,5]) set([0, 33, 3, 5, 45, 333, 2000, 1919]) |
对于数字
1 | 33 & (ht->num_buckets - 1) = 1 |
实际上是:
1 | '0b100001' & '0b111'= '0b1' # 1 the index of 33 |
注:在这种情况下,
对于
1 | '0b11101111111' & '0b111' = '0b111' # 7 the index of 1919 |
对于
1 | '0b101001101' & '0b111' = '0b101' # 5 the index of 333 |
有关python散列函数的更多详细信息,可以从python源代码中读取以下引号:
Major subtleties ahead: Most hash schemes depend on having a"good" hash
function, in the sense of simulating randomness. Python doesn't: its most
important hash functions (for strings and ints) are very regular in common
cases:
1
2
3
4 >>> map(hash, (0, 1, 2, 3))
[0, 1, 2, 3]
>>> map(hash, ("namea","nameb","namec","named"))
[-1658398457, -1658398460, -1658398459, -1658398462]This isn't necessarily bad! To the contrary, in a table of size 2**i, taking
the low-order i bits as the initial table index is extremely fast, and there
are no collisions at all for dicts indexed by a contiguous range of ints.
The same is approximately true when keys are"consecutive" strings. So this
gives better-than-random behavior in common cases, and that's very desirable.OTOH, when collisions occur, the tendency to fill contiguous slices of the
hash table makes a good collision resolution strategy crucial. Taking only
the last i bits of the hash code is also vulnerable: for example, consider
the list[i << 16 for i in range(20000)] as a set of keys. Since ints are their own hash codes, and this fits in a dict of size 2**15, the last 15 bits of every hash code are all 0: they all map to the same table index.But catering to unusual cases should not slow the usual ones, so we just take
the last i bits anyway. It's up to collision resolution to do the rest. If
we usually find the key we're looking for on the first try (and, it turns
out, we usually do -- the table load factor is kept under 2/3, so the odds
are solidly in our favor), then it makes best sense to keep the initial index
computation dirt cheap.
1 2 3 4 5 6 | class int: def __hash__(self): value = self if value == -1: value = -2 return value |
从python 3.7开始(并且已经在cpython 3.6中),字典项保持插入顺序。