Intentando entender el ciclo for Python, pensé que esto daría el resultado {1}
para una iteración, o simplemente quedaría atascado en un ciclo infinito, dependiendo de si lo hace como en C u otros lenguajes. Pero en realidad no lo hizo.
>>> s = {0}
>>> for i in s:
... s.add(i + 1)
... s.remove(i)
...
>>> print(s)
{16}
¿Por qué hace 16 iteraciones? De donde viene el resultado{16}
viene ?
Esto estaba usando Python 3.8.2. En pypy hace el resultado esperado {1}
.
python
python-internals
desbordamiento de novatos
fuente
fuente
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.t = {16}
y luegot.add(15)
arrojó que t es el conjunto {16, 15}. Creo que el problema está en alguna parte.Respuestas:
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
: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
: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 enset_discard_entry
:Cuando
4
se agrega al conjunto, el número de elementos y dummies en el conjunto se vuelve lo suficientemente alto como paraset_add_entry
desencadenar una reconstrucción de la tabla hash, llamandoset_table_resize
:so->used
es el número de entradas pobladas no ficticias en la tabla hash, que es 2, por lo queset_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: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.fuente
Creo que esto tiene algo que ver con la implementación real de conjuntos en python. Los conjuntos usan tablas hash para almacenar sus elementos, por lo que iterar sobre un conjunto significa iterar sobre las filas de su tabla hash.
A medida que itera y agrega elementos a su conjunto, se crean nuevos hashes y se agregan a la tabla hash hasta llegar al número 16. En este punto, el siguiente número se agrega al principio de la tabla hash y no al final. Y dado que ya iteraste sobre la primera fila de la tabla, el ciclo de iteración termina.
Mi respuesta se basa en esta pregunta similar, en realidad muestra exactamente el mismo ejemplo. Realmente recomiendo leerlo para más detalles.
fuente
De la documentación de Python 3:
Iterar sobre una copia
que debe iterar solo 1 vez
Editar: Una posible razón para esta iteración es porque un conjunto no está ordenado, lo que causa algún tipo de seguimiento de pila. Si lo hace con una lista y no con un conjunto, simplemente terminará,
s = [1]
porque las listas están ordenadas de modo que el ciclo for comenzará con el índice 0 y luego pasará al siguiente índice, encontrando que no hay uno, y saliendo del bucle.fuente
Python estableció una colección desordenada que no registra la posición del elemento ni el orden de inserción. No hay índice adjunto a ningún elemento en un conjunto de Python. Por lo tanto, no admiten ninguna operación de indexación o corte.
Así que no esperes que tu bucle for funcione en un orden definido.
¿Por qué hace 16 iteraciones?
user2357112 supports Monica
Ya explica la causa principal. Aquí, hay otra forma de pensar.Cuando ejecuta este código, le da salida a esto:
Cuando accedemos a todos los elementos juntos como un bucle o imprimiendo el conjunto, debe haber un orden predefinido para que atraviese todo el conjunto. Entonces, en la última iteración verá que el orden cambia de
{i,i+1}
a{i+1,i}
.Después de la última iteración sucedió que
i+1
ya está atravesado, así que salga del bucle.Dato interesante: Use cualquier valor menor que 16 excepto 6 y 7 siempre le dará el resultado 16.
fuente