为什么在迭代时添加到集合中和从集合中删除时会得到这么多迭代?


61

为了理解Python的for循环,我认为这会给出{1}一次迭代的结果,或者只是陷入无限循环,这取决于它是否像C语言或其他语言那样进行迭代。但是实际上它都没有。

>>> s = {0}
>>> for i in s:
...     s.add(i + 1)
...     s.remove(i)
...
>>> print(s)
{16}

为什么要进行16次迭代?结果{16}从何而来?

这是使用Python 3.8.2。在pypy上可以达到预期的效果{1}


17
根据您添加的项目,每次调用s.add(i+1)(可能还会调用s.remove(i))都可以更改集合的迭代顺序,从而影响到for循环创建的集合迭代器接下来将看到的内容。拥有活动的迭代器时,请勿使对象发生变化。
chepner

6
我也注意到了t = {16},然后t.add(15)得出t是集合{16,15}。我认为问题出在哪里。

19
这是一个实现细节-16的哈希值比15低(这是@Anon注意到的),因此将16添加到集合类型中将其添加到迭代器的“已经可见”部分,因此迭代器已用尽。
Błotosmętek

1
如果您阅读docs的资料,其中有一条注释说在循环期间更改迭代器可能会产生一些错误。请参阅:docs.python.org/3.7/reference/...
马塞罗法布里奇奥

3
@Błotosmętek:在CPython 3.8.2上,hash(16)== 16和hash(15)== 15。元素不会直接以哈希顺序存储在集合中。
user2357112支持Monica

Answers:


86

Python没有保证该循环何时结束(如果有的话)。在迭代过程中修改集会导致元素跳过,元素重复和其他怪异现象。永远不要依靠这种行为。

我要说的只是实现细节,如有更改,恕不另行通知。如果编写依赖于该程序的程序,则该程序可能会在Python实现和CPython 3.8.2以外的版本的任何组合上中断。

对于为什么循环在16处结束的简短解释是16是第一个元素,恰好位于比前一个元素更低的哈希表索引处。完整的解释如下。


Python集的内部哈希表始终具有2的幂。对于大小为2 ^ n的表,如果没有发生冲突,则将元素存储在哈希表中与它们的哈希的n个最低有效位相对应的位置。您可以在中看到此实现set_add_entry

mask = so->mask;
i = (size_t)hash & mask;

entry = &so->table[i];
if (entry->key == NULL)
    goto found_unused;

大多数小型Python int会自己散列;特别是,您测试中的所有整数都将自身哈希。您可以在中看到此实现long_hash。由于您的集合中永远不会包含两个哈希值相等的低位元素,因此不会发生冲突。


Python set迭代器使用简单的整数索引来跟踪其在集合中的位置,该整数索引进入该集合的内部哈希表。当请求下一个元素时,迭代器在哈希表中从该索引开始搜索填充的条目,然后将其存储的索引设置为找到的条目之后的立即,并返回该条目的元素。您可以在中看到此内容setiter_iternext

while (i <= mask && (entry[i].key == NULL || entry[i].key == dummy))
    i++;
si->si_pos = i+1;
if (i > mask)
    goto fail;
si->len--;
key = entry[i].key;
Py_INCREF(key);
return key;

您的集合最初从大小为8的哈希表开始,并指向0哈希表中索引为0 的int对象的指针。迭代器也位于索引0处。进行迭代时,将元素添加到哈希表中,每个元素都添加到下一个索引,因为这是哈希表示放置它们的位置,并且始终是迭代器查看的下一个索引。删除的元素在其旧位置存储了一个虚拟标记,以解决冲突。您可以在中看到实现set_discard_entry

entry = set_lookkey(so, key, hash);
if (entry == NULL)
    return -1;
if (entry->key == NULL)
    return DISCARD_NOTFOUND;
old_key = entry->key;
entry->key = dummy;
entry->hash = -1;
so->used--;
Py_DECREF(old_key);
return DISCARD_FOUND;

4添加到集合中时,集合中的元素和虚拟对象的数量变得足够高,从而set_add_entry触发哈希表重建,调用set_table_resize

if ((size_t)so->fill*5 < mask*3)
    return 0;
return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4);

so->used是哈希表中已填充的非虚拟条目的数量,为2,因此set_table_resize接收8作为其第二个参数。基于此,set_table_resize 决定新的哈希表大小应为16:

/* Find the smallest table size > minused. */
/* XXX speed-up with intrinsics */
size_t newsize = PySet_MINSIZE;
while (newsize <= (size_t)minused) {
    newsize <<= 1; // The largest possible value is PY_SSIZE_T_MAX + 1.
}

它会重建大小为16的哈希表。所有元素仍然以新哈希表中的旧索引结尾,因为它们的哈希值中没有设置任何高位。

随着循环的继续,元素将继续放置在迭代器将要查找的下一个索引处。触发了另一个哈希表重建,但新大小仍为16。

当循环添加16作为元素时,模式会中断。没有索引16来放置新元素。16的4个最低位是0000,在索引0处放置16。此时,迭代器的存储索引为16,并且当循环从迭代器中请求下一个元素时,迭代器会发现它已超出了索引的末尾。哈希表。

迭代器此时终止循环,仅保留16在集合中。


14

我相信这与python中集合的实际实现有关。集合使用哈希表来存储其项,因此迭代集合意味着迭代其哈希表的行。

在迭代并将项添加到集合中时,将创建新的哈希并将其附加到哈希表中,直到达到数字16。这时,实际上将下一个数字添加到哈希表的开头而不是结尾。并且由于您已经遍历了表的第一行,因此迭代循环结束了。

我的答案是基于这个类似的问题之一,它实际上显示了这个例子完全一样。我真的建议阅读它以获取更多详细信息。


5

从python 3文档中:

在一个集合上进行迭代时修改一个集合的代码可能很棘手。取而代之的是,遍历集合的副本或创建新的集合通常更直接:

遍历副本

s = {0}
s2 = s.copy()
for i in s2:
     s.add(i + 1)
     s.remove(i)

应该只迭代1次

>>> print(s)
{1}
>>> print(s2)
{0}

编辑:此迭代的可能原因是因为集合是无序的,从而导致某种类型的堆栈跟踪。如果您使用列表而不是集合来进行操作,那么它将以结尾而结束,s = [1]因为列表是有序的,因此for循环将从索引0开始,然后移至下一个索引,发现没有索引,并且退出循环。


是。但是我的问题是为什么它要进行16次迭代。
noob

集合是无序的。字典和设置以非随机顺序进行迭代,并且此迭代算法仅在您不进行任何修改的情况下成立。对于列表和元组,它只能按索引进行迭代。当我在3.7.2中尝试您的代码时,它进行了8次迭代。
Eric Jin

正如其他人所提到的,迭代顺序可能与哈希有关
Eric Jin

1
这是什么意思“导致某种堆栈跟踪排序问题”?代码没有导致崩溃或错误,所以我没有看到任何堆栈跟踪。如何在python中启用堆栈跟踪?
小白溢出

1

Python设置了一个无序集合,该集合不记录元素的位置或插入顺序。python集中的任何元素都没有索引。因此,它们不支持任何索引或切片操作。

因此,不要期望for循环会按定义的顺序工作。

为什么要进行16次迭代?

user2357112 supports Monica已经解释了主要原因。在这里,是另一种思维方式。

s = {0}
for i in s:
     s.add(i + 1)
     print(s)
     s.remove(i)
print(s)

运行此代码时,它会输出以下内容:

{0, 1}                                                                                                                               
{1, 2}                                                                                                                               
{2, 3}                                                                                                                               
{3, 4}                                                                                                                               
{4, 5}                                                                                                                               
{5, 6}                                                                                                                               
{6, 7}                                                                                                                               
{7, 8}
{8, 9}                                                                                                                               
{9, 10}                                                                                                                              
{10, 11}                                                                                                                             
{11, 12}                                                                                                                             
{12, 13}                                                                                                                             
{13, 14}                                                                                                                             
{14, 15}                                                                                                                             
{16, 15}                                                                                                                             
{16}       

当我们一起访问所有元素(例如循环或打印集合)时,必须有一个预定义的顺序才能遍历整个集合。因此,在最后一次迭代中,您将看到顺序从{i,i+1}变为{i+1,i}

在最后一次迭代之后,它i+1已经遍历了,因此循环退出。

有趣的事实: 使用小于16的任何值(6和7除外)将始终为您提供结果16。


“使用任何小于16的值将始终为您提供结果16。” -尝试使用6或7进行测试,您会发现那不成立。
user2357112支持Monica

@ user2357112支持Monica我更新了它。谢谢
Eklavya
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.