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-хэтов для себя; в частности, все целые в вашем тестовом хэше. Вы можете увидеть это реализовано в 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
объект int с индексом 0 в хеш-таблице. Итератор также расположен в индексе 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 для размещения нового элемента в. 4 младших бита из 16 равны 0000, что означает 16 с индексом 0. В этот момент сохраненный индекс итератора равен 16, и когда цикл запрашивает следующий элемент у итератора, итератор видит, что он прошел конец конца. хеш-таблица.
Итератор завершает цикл в этой точке, оставляя только 16
в наборе.
s.add(i+1)
(и, возможно, вызовs.remove(i)
) может изменять порядок итераций набора, влияя на то, что будет видеть следующий итератор набора, созданный циклом for. Не изменяйте объект, пока у вас есть активный итератор.