Python no promete cuándo (si alguna vez) finalizará este ciclo. La modificación de un conjunto durante la iteración puede conducir a elementos omitidos, elementos repetidos y otras rarezas. Nunca confíes en tal comportamiento.
Todo lo que voy a decir son detalles de implementación, sujetos a cambios sin previo aviso. Si escribe un programa que se basa en alguno de ellos, su programa puede interrumpir cualquier combinación de implementación y versión de Python que no sea CPython 3.8.2.
La breve explicación de por qué el ciclo termina en 16 es que 16 es el primer elemento que se coloca en un índice de tabla hash más bajo que el elemento anterior. La explicación completa está abajo.
La tabla hash interna de un conjunto de Python siempre tiene una potencia de 2 tamaños. Para una tabla de tamaño 2 ^ n, si no se producen colisiones, los elementos se almacenan en la posición en la tabla hash correspondiente a los n bits menos significativos de su hash. Puede ver esto implementado en set_add_entry
:
mask = so->mask;
i = (size_t)hash & mask;
entry = &so->table[i];
if (entry->key == NULL)
goto found_unused;
La mayoría de los pequeños Python se dividen a sí mismos; particularmente, todas las entradas en tu prueba se dividen. Puedes ver esto implementado en long_hash
. Como su conjunto nunca contiene dos elementos con bits bajos iguales en sus hashes, no se produce una colisión.
Un iterador de conjunto de Python realiza un seguimiento de su posición en un conjunto con un índice entero simple en la tabla hash interna del conjunto. Cuando se solicita el siguiente elemento, el iterador busca una entrada poblada en la tabla hash que comience en ese índice, luego establece su índice almacenado inmediatamente después de la entrada encontrada y devuelve el elemento de la entrada. Puedes ver esto en 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;
Inicialmente, su conjunto comienza con una tabla hash de tamaño 8 y un puntero a un 0
objeto int en el índice 0 en la tabla hash. El iterador también se posiciona en el índice 0. A medida que itera, los elementos se agregan a la tabla hash, cada uno en el siguiente índice porque allí es donde su hash dice ponerlos, y ese es siempre el siguiente índice que mira el iterador. Los elementos eliminados tienen un marcador ficticio almacenado en su posición anterior, con fines de resolución de colisiones. Puedes ver eso implementado en 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;
Cuando 4
se agrega al conjunto, el número de elementos y dummies en el conjunto se vuelve lo suficientemente alto como para set_add_entry
desencadenar una reconstrucción de la tabla hash, llamando 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
es el número de entradas pobladas no ficticias en la tabla hash, que es 2, por lo que set_table_resize
recibe 8 como segundo argumento. En base a esto, set_table_resize
decide que el nuevo tamaño de la tabla hash debe ser 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.
}
Reconstruye la tabla hash con tamaño 16. Todos los elementos aún terminan en sus antiguos índices en la nueva tabla hash, ya que no tenían ningún bit alto establecido en sus hashes.
A medida que el ciclo continúa, los elementos se siguen colocando en el siguiente índice que verá el iterador. Se activa otra reconstrucción de la tabla hash, pero el nuevo tamaño sigue siendo 16.
El patrón se rompe cuando el ciclo agrega 16 como elemento. No hay un índice 16 para colocar el nuevo elemento. Los 4 bits más bajos de 16 son 0000, poniendo 16 en el índice 0. El índice almacenado del iterador es 16 en este punto, y cuando el bucle solicita el siguiente elemento del iterador, el iterador ve que ha pasado el final del tabla de picadillo.
El iterador termina el ciclo en este punto, dejando solo 16
en el conjunto.
s.add(i+1)
(y posiblemente la llamada as.remove(i)
) puede alterar el orden de iteración del conjunto, afectando lo que verá el iterador de conjunto que creó el bucle for. No mutes un objeto mientras tengas un iterador activo.