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

问题描述 投票:23回答:4

试图了解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

python python-internals
4个回答
12
投票

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

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

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


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

set_add_entry

[大多数小型Python int会自己散列;特别是,您测试中的所有整数都将自身哈希。您可以在mask = so->mask; i = (size_t)hash & mask; entry = &so->table[i]; if (entry->key == NULL) goto found_unused; 中看到实现。由于您的集合中永远不会包含两个哈希值相等的低位元素,因此不会发生冲突。


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

long_hash

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

[将0添加到集合中时,集合中的元素和虚拟对象的数量变得足够高,以致set_discard_entry触发哈希表重建,并调用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是哈希表中已填充的非虚拟条目的数量,为2,因此set_add_entry收到8作为其第二个参数。基于此,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); 新哈希表的大小应为16:]

so->used

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

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

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

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


8
投票

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


3
投票

摘自python 3文档:


1
投票

签出this。无法保证Python遍历该集合的顺序,只是如果该集合不变则将是相同的。如果您在循环中添加打印语句,如下所示:

© www.soinside.com 2019 - 2024. All rights reserved.