Python nie obiecuje, kiedy (jeśli w ogóle) ta pętla się zakończy. Modyfikowanie zestawu podczas iteracji może prowadzić do pomijania elementów, powtarzania elementów i innych dziwności. Nigdy nie polegaj na takim zachowaniu.
Wszystko, co powiem, to szczegóły implementacji, które mogą ulec zmianie bez powiadomienia. Jeśli napiszesz program, który opiera się na którymkolwiek z nich, Twój program może złamać dowolną kombinację implementacji Pythona i wersji innej niż CPython 3.8.2.
Krótkim wyjaśnieniem, dlaczego pętla kończy się na 16, jest to, że 16 jest pierwszym elementem, który znajduje się przy niższym indeksie tablicy skrótów niż poprzedni element. Pełne wyjaśnienie znajduje się poniżej.
Wewnętrzna tabela skrótów zestawu Python ma zawsze moc 2 rozmiarów. W przypadku tabeli o wielkości 2 ^ n, jeśli nie wystąpią kolizje, elementy są zapisywane w pozycji w tablicy skrótów odpowiadającej n najmniej znaczącym bitom ich skrótu. Możesz to zobaczyć zaimplementowane w set_add_entry
:
mask = so->mask;
i = (size_t)hash & mask;
entry = &so->table[i];
if (entry->key == NULL)
goto found_unused;
Większość małych skrótów Pythona do siebie; szczególnie wszystkie ints w twoim skrócie testowym do siebie. Możesz to zobaczyć zaimplementowane w long_hash
. Ponieważ twój zestaw nigdy nie zawiera w swoim haszu dwóch elementów z jednakowymi małymi bitami, nie dochodzi do kolizji.
Iterator zestawu Python śledzi jego pozycję w zestawie za pomocą prostego indeksu liczb całkowitych w wewnętrznej tabeli skrótów zestawu. Kiedy żądany jest następny element, iterator szuka zapełnionego wpisu w tablicy mieszającej, zaczynając od tego indeksu, a następnie ustawia przechowywany indeks na natychmiast po znalezionym wpisie i zwraca element wpisu. Możesz to zobaczyć w 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;
Zestaw początkowo zaczyna się od tablicy skrótów o rozmiarze 8 i wskaźnikiem do 0
obiektu int o indeksie 0 w tablicy skrótów. Iterator jest również pozycjonowany na indeksie 0. Podczas iteracji elementy są dodawane do tabeli mieszającej, każdy przy kolejnym indeksie, ponieważ tam właśnie ich hash mówi, aby je umieścić, i to zawsze jest następny indeks, na który patrzy iterator. Usunięte elementy mają atrapę znacznika zapisaną w ich starej pozycji do celów rozwiązywania kolizji. Możesz to zobaczyć zaimplementowane w 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;
Po 4
dodaniu do zestawu liczba elementów i manekinów w zestawie staje się na tyle wysoka, że set_add_entry
wyzwala przebudowę tabeli skrótów, wywołując 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
jest liczbą zapełnionych, nieobojętnych wpisów w tablicy mieszającej, która wynosi 2, więc set_table_resize
otrzymuje 8 jako drugi argument. Na tej podstawie set_table_resize
decyduje, że nowy rozmiar tabeli skrótów powinien wynosić 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.
}
Odbudowuje tabelę skrótów o rozmiarze 16. Wszystkie elementy wciąż kończą się na swoich starych indeksach w nowej tabeli skrótów, ponieważ nie miały żadnych wysokich bitów w swoich skrótach.
W miarę trwania pętli elementy są umieszczane przy następnym indeksie, w którym będzie wyglądał iterator. Uruchomiona zostaje kolejna przebudowa tabeli skrótów, ale nowy rozmiar to wciąż 16.
Wzór łamie się, gdy pętla dodaje 16 jako element. Nie ma indeksu 16, w którym można umieścić nowy element. 4 najniższe bity 16 to 0000, co oznacza, że 16 ma indeks 0. W tym momencie przechowywany indeks iteratora wynosi 16, a gdy pętla prosi o kolejny element z iteratora, iterator widzi, że przekroczył koniec tabela mieszania.
W tym momencie iterator kończy pętlę, pozostawiając tylko 16
w zestawie.
s.add(i+1)
(i ewentualnie wywołanies.remove(i)
) może zmienić kolejność iteracji zestawu, wpływając na to, co zobaczy iterator zestawu utworzony przez pętlę for. Nie mutuj obiektu, gdy masz aktywny iterator.