Python macht keine Versprechungen darüber, wann (wenn überhaupt) diese Schleife enden wird. Das Ändern eines Satzes während der Iteration kann zu übersprungenen Elementen, wiederholten Elementen und anderen Verrücktheiten führen. Verlassen Sie sich niemals auf ein solches Verhalten.
Alles, was ich sagen möchte, sind Implementierungsdetails, die ohne vorherige Ankündigung geändert werden können. Wenn Sie ein Programm schreiben, das auf einem dieser Programme basiert, kann Ihr Programm bei einer beliebigen Kombination aus Python-Implementierung und einer anderen Version als CPython 3.8.2 beschädigt werden.
Die kurze Erklärung dafür, warum die Schleife bei 16 endet, ist, dass 16 das erste Element ist, das zufällig an einem niedrigeren Hash-Tabellenindex als das vorherige Element platziert wird. Die vollständige Erklärung finden Sie unten.
Die interne Hash-Tabelle eines Python-Sets hat immer eine Potenz von 2. Wenn für eine Tabelle der Größe 2 ^ n keine Kollisionen auftreten, werden Elemente an der Position in der Hash-Tabelle gespeichert, die den n niedrigstwertigen Bits ihres Hash entspricht. Sie können dies implementiert sehen in set_add_entry
:
mask = so->mask;
i = (size_t)hash & mask;
entry = &so->table[i];
if (entry->key == NULL)
goto found_unused;
Die meisten kleinen Python-Ints-Hash für sich; Insbesondere alle Ints in Ihrem Test-Hash für sich. Sie können dies in implementiert sehen long_hash
. Da Ihre Menge niemals zwei Elemente mit gleich niedrigen Bits in ihren Hashes enthält, tritt keine Kollision auf.
Ein Python-Set-Iterator verfolgt seine Position in einem Set mit einem einfachen Integer-Index in der internen Hash-Tabelle des Sets. Wenn das nächste Element angefordert wird, sucht der Iterator ab diesem Index nach einem ausgefüllten Eintrag in der Hash-Tabelle, setzt dann seinen gespeicherten Index auf unmittelbar nach dem gefundenen Eintrag und gibt das Element des Eintrags zurück. Sie können dies sehen in 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;
Ihre Menge beginnt zunächst mit einer Hash-Tabelle der Größe 8 und einem Zeiger auf ein 0
int-Objekt am Index 0 in der Hash-Tabelle. Der Iterator befindet sich ebenfalls am Index 0. Während Sie iterieren, werden der Hash-Tabelle Elemente hinzugefügt, jeweils am nächsten Index, da dort der Hash sagt, dass sie abgelegt werden sollen, und dies ist immer der nächste Index, den der Iterator betrachtet. Bei entfernten Elementen wird zur Kollisionsauflösung eine Dummy-Markierung an ihrer alten Position gespeichert. Sie können sehen, dass implementiert in 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;
Wenn 4
der Menge hinzugefügt wird, wird die Anzahl der Elemente und Dummies in der Menge hoch genug, um set_add_entry
eine Neuerstellung der Hash-Tabelle auszulösen und Folgendes aufzurufen 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
ist die Anzahl der ausgefüllten Nicht-Dummy-Einträge in der Hash-Tabelle, die 2 ist, und set_table_resize
erhält daher 8 als zweites Argument. Basierend darauf wird entschieden, set_table_resize
dass die neue Hash-Tabellengröße 16 sein sollte:
/* 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.
}
Die Hash-Tabelle mit Größe 16 wird neu erstellt. Alle Elemente landen weiterhin an ihren alten Indizes in der neuen Hash-Tabelle, da in ihren Hashes keine hohen Bits gesetzt waren.
Während die Schleife fortgesetzt wird, werden Elemente immer wieder am nächsten Index platziert, nach dem der Iterator suchen wird. Eine weitere Neuerstellung der Hash-Tabelle wird ausgelöst, die neue Größe beträgt jedoch noch 16.
Das Muster wird unterbrochen, wenn die Schleife 16 als Element hinzufügt. Es gibt keinen Index 16, an dem das neue Element platziert werden kann. Die 4 niedrigsten Bits von 16 sind 0000, wobei 16 auf Index 0 gesetzt wird. Der gespeicherte Index des Iterators ist zu diesem Zeitpunkt 16, und wenn die Schleife vom Iterator nach dem nächsten Element fragt, sieht der Iterator, dass das Ende des Iterators überschritten wurde Hash-tabelle.
Der Iterator beendet die Schleife an dieser Stelle und belässt nur 16
die Menge.
s.add(i+1)
(und möglicherweise der Aufruf vons.remove(i)
) die Iterationsreihenfolge des Satzes ändern, was sich darauf auswirkt, was der von der for-Schleife erstellte Satziterator als Nächstes sieht. Mutieren Sie kein Objekt, während Sie einen aktiven Iterator haben.