Python не дає жодних обіцянок про те, коли (якщо взагалі) ця петля закінчиться. Змінення набору під час ітерації може призвести до пропущених елементів, повторних елементів та інших дивацтв. Ніколи не покладайтеся на таку поведінку.
Все, що я збираюся сказати, - це деталі щодо впровадження, можуть змінюватися без попереднього повідомлення. Якщо ви пишете програму, яка спирається на будь-яку з них, ваша програма може перерватися на будь-яку комбінацію реалізації Python та іншої версії, крім CPython 3.8.2.
Коротке пояснення того, чому цикл закінчується на 16, полягає в тому, що 16 - це перший елемент, який, можливо, розміщується з нижчим індексом хеш-таблиці, ніж попередній елемент. Повне пояснення наведено нижче.
Внутрішня хеш-таблиця набору Python завжди має 2 розміри. Для таблиці розміром 2 ^ 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)
) може змінювати порядок ітерації набору, впливаючи на те, що побачить наступний ітератор встановленого циклу. Не мутуйте об'єкт під час активного ітератора.