无序Python集的“顺序”

时间:2012-08-28 18:20:31

标签: python python-internals

我知道Python中的集合是无序的,但我对它们显示的“顺序”感到好奇,因为它似乎是一致的。它们似乎每次都以同样的方式乱序:

>>> set_1 = set([5, 2, 7, 2, 1, 88])
>>> set_2 = set([5, 2, 7, 2, 1, 88])
>>> set_1
set([88, 1, 2, 5, 7])
>>> set_2
set([88, 1, 2, 5, 7])

......和另一个例子:

>>> set_3 = set('abracadabra')
>>> set_4 = set('abracadabra')
>>> set_3
set(['a', 'r', 'b', 'c', 'd'])
>>>> set_4
set(['a', 'r', 'b', 'c', 'd'])

我只是好奇为什么会这样。有什么帮助吗?

5 个答案:

答案 0 :(得分:38)

你应该看这个video(虽然它是CPython 1 特定的和关于字典 - 但我认为它也适用于集合)。

基本上,python散列元素并获取最后N位(其中N由集合的大小确定)并使用这些位作为数组索引将对象放置在内存中。然后按照它们存在于存储器中的顺序产生对象。当然,当您需要解决哈希之间的冲突时,图片会变得更复杂,但这就是它的要点。

另请注意,打印出来的顺序取决于您放入的顺序(由于碰撞)。因此,如果您重新排序传递给set_2的列表,如果存在关键冲突,您可能会获得不同的订单。

例如:

list1 = [8,16,24]
set(list1)        #set([8, 16, 24])
list2 = [24,16,8]
set(list2)        #set([24, 16, 8])

注意订单在这些集合中保留的事实是“巧合”,并且与冲突解决(我不知道任何事情)有关。关键是hash(8)hash(16)hash(24)的最后3位是相同的。因为它们是相同的,所以碰撞解决方案会接管并将元素放在“备份”内存位置而不是第一个(最佳)选择中,因此8占用位置还是16取决于哪一个首先到达了聚会,并获得了“最佳席位”。

如果我们使用123重复该示例,无论订单在输入列表中的顺序如何,您都会得到一致的订单:

list1 = [1,2,3]
set(list1)      # set([1, 2, 3])
list2 = [3,2,1]
set(list2)      # set([1, 2, 3])

由于hash(1)的最后3位,hash(2)hash(3)是唯一的。


1 注意此处描述的实施适用于CPython dictset。我认为一般描述适用于所有现代版本的CPython,最高可达3.6。但是,从CPython3.6开始,还有一个额外的实现细节,实际上保留了dict的迭代的插入顺序。似乎set仍然没有此属性。数据结构由this blog post由pypy伙伴(在CPython人员之前开始使用它)描述。最初的想法(至少对于python生态系统)is archived on the python-dev mailing list

答案 1 :(得分:4)

这种行为的原因是Python使用哈希表进行字典实现:https://en.wikipedia.org/wiki/Hash_table#Open_addressing

密钥的位置由其内存地址定义。如果你知道某些对象的Python重用内存:

>>> a = 'Hello world'
>>> id(a)
140058096568768
>>> a = 'Hello world'
>>> id(a)
140058096568480

您可以看到对象 a 每次初始化时都有不同的地址。

但对于小整数而言,它并没有改变:

>>> a = 1
>>> id(a)
40060856
>>> a = 1
>>> id(a)
40060856

即使我们使用不同的名称创建第二个对象,它也是相同的:

>>> b = 1
>>> id(b)
40060856

这种方法可以节省Python解释器消耗的内存。

答案 2 :(得分:3)

AFAIK Python集使用hash table实现。项目出现的顺序取决于使用的哈希函数。在程序的同一次运行中,哈希函数可能不会改变,因此您获得相同的顺序。

但是无法保证它将始终使用相同的函数,并且顺序将在运行中更改 - 或者如果插入大量元素并且哈希表必须调整大小,则在同一运行中更改。

答案 3 :(得分:2)

集基于哈希表。值的散列应该是一致的,所以顺序也是 - 除非两个元素散列到相同的代码,在这种情况下,插入的顺序将改变输出顺序。

答案 4 :(得分:1)

mgilson's great answer提示的一个关键事项,但在任何现有答案中均未明确提及:

小整数会散列到自己的位置:

>>> [hash(x) for x in (1, 2, 3, 88)]
[1, 2, 3, 88]

字符串散列到不可预测的值。实际上,从3.3开始,默认情况下为they're built off a seed that's randomized at startup。因此,对于每个新的Python解释器会话,您将获得不同的结果,但是:

>>> [hash(x) for x in 'abcz']
[6014072853767888837,
 8680706751544317651,
 -7529624133683586553,
 -1982255696180680242]

因此,请考虑最简单的哈希表实现:仅由N个元素组成的数组,其中插入值意味着将其放入hash(value) % N中(假设没有冲突)。您可以粗略估计N的大小,它会比其中的元素数大一点。当由6个元素的序列创建集合时,N很容易说成8。

当您以N = 8存储这5个数字时会发生什么?好吧,hash(1) % 8hash(2) % 8等只是数字本身,而hash(88) % 8为0。因此,哈希表的数组最终保存为88, 1, 2, NULL, NULL, 5, NULL, 7。因此,应该很容易弄清楚为什么迭代集合可能会给您88, 1, 2, 5, 7

当然,Python不能保证,您每次都会得到此命令。对N的正确值的猜测方式稍作更改,可能意味着88最终会出现在其他地方(或最终与其他值之一冲突)。而且,实际上,在Mac上运行CPython 3.7时,我得到1, 2, 5, 7, 88。0

同时,当您从大小为11的序列中构建一个哈希,然后将随机哈希插入其中时,会发生什么?即使假设最简单的实现,并且假设没有冲突,您仍然不知道要获得什么顺序。一次运行Python解释器将保持一致,但下次启动时将有所不同。 (除非您将PYTHONHASHSEED设置为0或其他一些int值。)这正是您所看到的。


当然值得关注the way sets are actually implemented而不是猜测。但是,基于最简单的哈希表实现的假设,您会猜测到底是什么(禁止冲突和禁止哈希表的扩展)。