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 集合迭代器通过一个简单的整数索引到集合的内部哈希表中来跟踪它在集合中的位置。当请求下一个元素时,迭代器从该索引开始在哈希表中搜索填充的条目,然后将其存储的索引设置为紧随找到的条目并返回条目的元素。你可以在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_resizedecides新的哈希表大小应该是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,将 16 放在索引 0 处。此时迭代器存储的索引是 16,当循环向迭代器请求下一个元素时,迭代器看到它已经超过了哈希表。
迭代器此时终止循环,只在集合中留下16。