Eliminación de elementos de la lista: durante la iteración, ¿qué hay de malo con este idioma?

Como experimento, hice esto:

letters=['a','b','c','d','e','f','g','h','i','j','k','l'] for i in letters: letters.remove(i) print letters 

La última impresión muestra que no se eliminaron todos los elementos? (Todos los demás eran).

 IDLE 2.6.2 >>> ================================ RESTART ================================ >>> ['b', 'd', 'f', 'h', 'j', 'l'] >>> 

¿Cuál es la explicación para esto? ¿Cómo podría reescribirse para eliminar todos los elementos?

Algunas respuestas explican por qué sucede esto y otras explican lo que deberías haber hecho. Voy a poner las piezas desvergonzadamente juntas.


¿Cuál es la razón de esto?

Debido a que el lenguaje Python está diseñado para manejar este caso de uso de manera diferente. La documentación lo aclara:

No es seguro modificar la secuencia que se está iterando en el bucle (esto solo puede suceder con los tipos de secuencia mutables, como las listas). Si necesita modificar la lista sobre la que está iterando (por ejemplo, para duplicar elementos seleccionados), debe iterar sobre una copia .

Énfasis mío. Consulte la página vinculada para obtener más información: la documentación tiene derechos de autor y todos los derechos están reservados.

Podría comprender fácilmente por qué obtuvo lo que recibió, pero es básicamente un comportamiento indefinido que puede cambiar fácilmente sin aviso de comstackción a comstackción. Simplemente no lo hagas.

Es como preguntarme por qué i += i++ + ++i lo que sea que hace esa línea en su architecture en su comstackción específica de su comstackdor para su idioma , que incluye, entre otras cosas, destruir su computadora y hacer que los demonios salgan volando de tu nariz 🙂


¿Cómo podría reescribirse para eliminar todos los elementos?

  • del letters[:] (si necesita cambiar todas las referencias a este objeto)
  • letters[:] = [] (si necesita cambiar todas las referencias a este objeto)
  • letters = [] (si solo quieres trabajar con un nuevo objeto)

¿Tal vez solo desea eliminar algunos elementos en función de una condición? En ese caso, debe iterar sobre una copia de la lista. La forma más fácil de hacer una copia es hacer una porción que contenga toda la lista con la syntax [:] , así:

 #remove unsafe commands commands = ["ls", "cd", "rm -rf /"] for cmd in commands[:]: if "rm " in cmd: commands.remove(cmd) 

Si su cheque no es particularmente complicado, puede (y probablemente debería) filtrar en su lugar:

 commands = [cmd for cmd in commands if not is_malicious(cmd)] 

No puede iterar sobre una lista y mutarla al mismo tiempo, en lugar de iterar sobre una porción:

 letters=['a','b','c','d','e','f','g','h','i','j','k','l'] for i in letters[:]: # note the [:] creates a slice letters.remove(i) print letters 

Dicho esto, para una operación simple como esta, simplemente debes usar:

 letters = [] 

No puedes modificar la lista que estás iterando, de lo contrario obtendrás este tipo de resultado extraño. Para hacer esto, debe iterar sobre una copia de la lista:

 for i in letters[:]: letters.remove(i) 

Elimina la primera aparición y, a continuación, comprueba el siguiente número en la secuencia. Como la secuencia ha cambiado, toma el siguiente número impar y así sucesivamente …

  • Tomar un”
  • eliminar “a” -> el primer elemento es ahora “b”
  • tomar el siguiente elemento, “c” -…

lo que quieres hacer es

 letters[:] = [] 

o

 del letters[:] 

Esto mantendrá el objeto original al que apuntaban las letters . Otras opciones como, letters = [] , crearían un nuevo objeto y señalarían letters : el objeto antiguo normalmente se recolectaría como basura después de un tiempo.

La razón por la que no se eliminaron todos los valores es que está cambiando la lista mientras se itera sobre ella.

ETA : si desea filtrar los valores de una lista, puede usar listas de comprensión como esta:

 >>> letters=['a','b','c','d','e','f','g','h','i','j','k','l'] >>> [l for l in letters if ord(l) % 2] ['a', 'c', 'e', 'g', 'i', 'k'] 

Probablemente Python usa punteros y la eliminación comienza en la parte frontal. La variable “letras” en la segunda línea tiene parcialmente un valor diferente al de la variable “letras” en la tercera línea. Cuando i es 1, a se está eliminando a, cuando i es 2, entonces b se ha movido a la posición 1 y c se está eliminando. Puedes intentar usar „while“.

  #!/usr/bin/env python import random a=range(10) while len(a): print a for i in a[:]: if random.random() > 0.5: print "removing: %d" % i a.remove(i) else: print "keeping: %d" % i print "done!" a=range(10) while len(a): print a for i in a: if random.random() > 0.5: print "removing: %d" % i a.remove(i) else: print "keeping: %d" % i print "done!" 

Creo que esto explica el problema un poco mejor, el bloque de código superior funciona, mientras que el de abajo no lo hace.

Los elementos que se “guardan” en la lista inferior nunca se imprimen, porque está modificando la lista sobre la que está iterando, que es una receta para el desastre.

Bien, llego un poco tarde a la fiesta aquí, pero he estado pensando en esto y después de ver el código de implementación de Python (CPython), tengo una explicación que me gusta. Si alguien sabe por qué es tonto o incorrecto, agradecería saber por qué.

El problema se está moviendo a través de una lista usando un iterador, mientras que permite que esa lista cambie.

Todo lo que el iterador está obligado a hacer es decirle qué elemento de la lista (en este caso) viene después del elemento actual (es decir, con la función next ()).

Creo que la forma en que se implementan actualmente los iteradores, solo hacen un seguimiento del índice del último elemento sobre el que se iteraron. Mirando en iterobject.c se puede ver lo que parece ser una definición de un iterador:

 typedef struct { PyObject_HEAD Py_ssize_t it_index; PyObject *it_seq; /* Set to NULL when iterator is exhausted */ } seqiterobject; 

donde it_seq apunta a la secuencia que se está it_index e it_index proporciona el índice del último elemento suministrado por el iterador.

Cuando el iterador acaba de proporcionar el elemento nth y uno elimina ese elemento de la secuencia, la correspondencia entre los elementos de lista subsiguientes y sus índices cambia. El primer (n + 1) elemento st se convierte en el n º elemento en lo que respecta al iterador. En otras palabras, el iterador ahora piensa que lo que era el ítem ‘siguiente’ en la secuencia es en realidad el ítem ‘actual’.

Por lo tanto, cuando se le pida que entregue el siguiente artículo, le dará el anterior (n + 2) elemento nd (es decir, el nuevo (n + 1) elemento st ).

Como resultado, para el código en cuestión, el método next() del iterador dará solo los elementos n + 0, n + 2, n + 4, … de la lista original. Los elementos n + 1, n + 3, n + 5, … nunca se expondrán a la instrucción de remove .

Aunque la actividad prevista del código en cuestión es clara (al menos para una persona), probablemente se requiera mucha más introspección para que un iterador monitoree los cambios en la secuencia sobre la que se repite y, luego, que actúe de una manera “humana”. .

Si los iteradores pueden devolver elementos anteriores o actuales de una secuencia, puede haber una solución general, pero tal como está, debe recorrer una copia de la lista y asegurarse de no eliminar ningún elemento antes de que el iterador llegue a ellos.