Python ne fait aucune promesse quant au moment (si jamais) cette boucle se terminera. La modification d'un ensemble pendant l'itération peut entraîner des éléments ignorés, des éléments répétés et d'autres bizarreries. Ne vous fiez jamais à un tel comportement.
Tout ce que je vais dire est des détails de mise en œuvre, sujets à changement sans préavis. Si vous écrivez un programme qui s'appuie sur l'un d'entre eux, votre programme peut se casser sur n'importe quelle combinaison d'implémentation Python et de version autre que CPython 3.8.2.
La courte explication de la fin de la boucle à 16 est que 16 est le premier élément qui se trouve être placé à un index de table de hachage inférieur à l'élément précédent. L'explication complète est ci-dessous.
La table de hachage interne d'un ensemble Python a toujours une puissance de 2 tailles. Pour une table de taille 2 ^ n, si aucune collision ne se produit, les éléments sont stockés à la position dans la table de hachage correspondant aux n bits de poids faible de leur hachage. Vous pouvez voir cela implémenté dans set_add_entry
:
mask = so->mask;
i = (size_t)hash & mask;
entry = &so->table[i];
if (entry->key == NULL)
goto found_unused;
La plupart des petits Python se hachent eux-mêmes; en particulier, toutes les valeurs de votre test de hachage pour elles-mêmes. Vous pouvez voir cela implémenté dans long_hash
. Puisque votre ensemble ne contient jamais deux éléments avec des bits bas égaux dans leurs hachages, aucune collision ne se produit.
Un itérateur d'ensemble Python garde une trace de sa position dans un ensemble avec un index entier simple dans la table de hachage interne de l'ensemble. Lorsque l'élément suivant est demandé, l'itérateur recherche une entrée remplie dans la table de hachage à partir de cet index, puis définit son index stocké immédiatement après l'entrée trouvée et renvoie l'élément de l'entrée. Vous pouvez le voir danssetiter_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;
Votre ensemble commence initialement par une table de hachage de taille 8 et un pointeur vers un 0
objet int à l'index 0 dans la table de hachage. L'itérateur est également positionné à l'index 0. Au fur et à mesure de votre itération, des éléments sont ajoutés à la table de hachage, chacun à l'index suivant, car c'est là que leur hachage dit de les mettre, et c'est toujours le prochain index que l'itérateur examine. Les éléments supprimés ont un marqueur factice stocké à leur ancienne position, à des fins de résolution de collision. Vous pouvez voir cela implémenté dans 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;
Lorsque 4
est ajouté à l'ensemble, le nombre d'éléments et de mannequins dans l'ensemble devient suffisamment élevé pourset_add_entry
déclencher une reconstruction de table de hachage, en appelant 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
est le nombre d'entrées non factices remplies dans la table de hachage, qui est 2, donc set_table_resize
reçoit 8 comme deuxième argument. Sur cette base, set_table_resize
décide que la nouvelle taille de la table de hachage doit être 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.
}
Il reconstruit la table de hachage avec la taille 16. Tous les éléments se retrouvent toujours à leurs anciens index dans la nouvelle table de hachage, car ils n'avaient pas de bits hauts définis dans leurs hachages.
Pendant que la boucle continue, les éléments continuent d'être placés au prochain index que l'itérateur regardera. Une autre reconstruction de table de hachage est déclenchée, mais la nouvelle taille est toujours de 16.
Le motif se rompt lorsque la boucle ajoute 16 en tant qu'élément. Il n'y a pas d'index 16 pour placer le nouvel élément. Les 4 bits les plus bas de 16 sont 0000, mettant 16 à l'index 0. L'index stocké de l'itérateur est 16 à ce stade, et lorsque la boucle demande l'élément suivant à l'itérateur, l'itérateur voit qu'il a dépassé la fin de la table de hachage.
L'itérateur termine la boucle à ce stade, ne laissant que 16
dans l'ensemble.
s.add(i+1)
(et éventuellement, l'appel às.remove(i)
) peut modifier l'ordre d'itération de l'ensemble, affectant ce que l'itérateur d'ensemble que la boucle for créée verra ensuite. Ne mute pas un objet tant que tu as un itérateur actif.