59

我知道 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'])

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

4

5 回答 5

52

你应该观看这个视频(虽然它是 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])

请注意,这些集合中保留的顺序是“巧合”,并且与冲突解决有关(我对此一无所知)。关键是 , 和 的最后 3 位hash(8)hash(16)相同hash(24)的。因为它们是相同的,所以冲突解决会接管并将元素放在“备份”内存位置而不是第一个(最佳)选择中,因此是否8占据一个位置或16取决于哪个首先到达聚会并采取“最佳座位”。

如果我们用1,2和重复该示例,3无论它们在输入列表中的顺序如何,您都将获得一致的顺序:

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

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


1注意这里描述的实现适用于 CPythondictset. 我认为一般描述适用于所有现代版本的 CPython,直到 3.6。但是,从 CPython3.6 开始,有一个额外的实现细节实际上保留了dict. 看来set还是没有这个属性。pypy 人(他们在 CPython 人之前就开始使用它)在这篇博文中描述了数据结构。最初的想法(至少对于 python 生态系统)存档在 python-dev 邮件列表中

于 2012-08-28T18:23:18.547 回答
6

这种行为的原因是 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 解释器消耗的内存。

于 2015-09-09T17:09:25.903 回答
3

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

但是不能保证它总是使用相同的函数,并且顺序会在运行之间发生变化 - 或者如果您插入大量元素并且哈希表必须调整大小,则在同一次运行中。

于 2012-08-28T18:29:57.183 回答
2

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

于 2012-08-28T18:23:26.137 回答
1

暗示mgilson 的好答案的关键一件事,但在任何现有答案中都没有明确提及:

小整数自己散列:

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

字符串散列为不可预测的值。事实上,从 3.3 开始,默认情况下,它们是基于启动时随机分配的种子构建的。因此,对于每个新的 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 解释器的一次运行中保持一致,但在您下次启动它时会有所不同。(除非您设置PYTHONHASHSEED0,或其他一些 int 值。)这正是您所看到的。


当然,值得关注的是集合的实际实现方式,而不是猜测。但是,根据最简单的哈希表实现的假设,您会猜到(排除冲突和哈希表扩展)究竟会发生什么。

于 2018-07-09T17:55:54.760 回答