为了理解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}
。
为了理解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}
。
Answers:
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
在集合中。
从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开始,然后移至下一个索引,发现没有索引,并且退出循环。
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。
s.add(i+1)
(可能还会调用s.remove(i)
)都可以更改集合的迭代顺序,从而影响到for循环创建的集合迭代器接下来将看到的内容。拥有活动的迭代器时,请勿使对象发生变化。