Python non promette quando (se mai) questo ciclo finirà. La modifica di un set durante l'iterazione può portare a elementi saltati, elementi ripetuti e altre stranezze. Non fare mai affidamento su tale comportamento.
Tutto quello che sto per dire sono i dettagli di implementazione, soggetti a modifiche senza preavviso. Se si scrive un programma che si basa su uno di essi, il programma potrebbe interrompere qualsiasi combinazione di implementazione e versione di Python diversa da CPython 3.8.2.
La breve spiegazione del perché il loop termina in 16 è che 16 è il primo elemento che si trova in un indice di tabella hash inferiore rispetto all'elemento precedente. La spiegazione completa è di seguito.
La tabella hash interna di un set Python ha sempre una potenza di 2 dimensioni. Per una tabella di dimensioni 2 ^ n, se non si verificano collisioni, gli elementi vengono memorizzati nella posizione nella tabella hash corrispondente ai n bit meno significativi del loro hash. Puoi vederlo implementato in set_add_entry
:
mask = so->mask;
i = (size_t)hash & mask;
entry = &so->table[i];
if (entry->key == NULL)
goto found_unused;
La maggior parte dei piccoli Python inserisce l'hash in se stessi; in particolare, tutti gli ints nel tuo test hanno hash verso se stessi. Puoi vederlo implementato in long_hash
. Dato che il tuo set non contiene mai due elementi con bit bassi uguali nei loro hash, non si verifica alcuna collisione.
Un iteratore di set Python tiene traccia della sua posizione in un set con un semplice indice intero nella tabella hash interna del set. Quando viene richiesto l'elemento successivo, l'iteratore cerca una voce popolata nella tabella hash a partire da quell'indice, quindi imposta l'indice memorizzato immediatamente dopo la voce trovata e restituisce l'elemento della voce. Puoi vederlo 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;
Il set inizia inizialmente con una tabella hash di dimensioni 8 e un puntatore a un 0
oggetto int all'indice 0 nella tabella hash. L'iteratore è anche posizionato sull'indice 0. Mentre esegui l'iterazione, gli elementi vengono aggiunti alla tabella hash, ciascuno nell'indice successivo perché è lì che il loro hash dice di metterli, ed è sempre l'indice successivo che l'iteratore osserva. Gli elementi rimossi hanno un indicatore fittizio memorizzato nella loro vecchia posizione, ai fini della risoluzione delle collisioni. Puoi vederlo implementato 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;
Quando 4
viene aggiunto al set, il numero di elementi e manichini nel set diventa abbastanza alto da set_add_entry
innescare una ricostruzione della tabella hash, chiamando 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
è il numero di voci popolate e non fittizie nella tabella hash, che è 2, quindi set_table_resize
riceve 8 come secondo argomento. Sulla base di questo, set_table_resize
decide che la nuova dimensione della tabella hash dovrebbe essere 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.
}
Ricostruisce la tabella hash con la dimensione 16. Tutti gli elementi finiscono ancora nei loro vecchi indici nella nuova tabella hash, poiché non avevano bit alti impostati nei loro hash.
Man mano che il ciclo continua, gli elementi continuano a essere posizionati nell'indice successivo a cui apparirà l'iteratore. Viene attivata un'altra ricostruzione della tabella hash, ma la nuova dimensione è ancora 16.
Il pattern si interrompe quando il loop aggiunge 16 come elemento. Non esiste un indice 16 in cui posizionare il nuovo elemento. I 4 bit più bassi di 16 sono 0000, ponendo 16 all'indice 0. L'indice memorizzato dell'iteratore è 16 a questo punto, e quando il ciclo richiede l'elemento successivo dall'iteratore, l'iteratore vede che è passato oltre la fine del tabella hash.
L'iteratore termina il ciclo a questo punto, lasciando solo 16
nel set.
s.add(i+1)
(e possibilmente, la chiamata as.remove(i)
) può modificare l'ordine di iterazione del set, influenzando ciò che vedrà il successivo iteratore creato dal ciclo for. Non mutare un oggetto mentre hai un iteratore attivo.