Eliminar de una lista mientras se itera sobre ella

El siguiente código:

a = list(range(10)) remove = False for b in a: if remove: a.remove(b) remove = not remove print(a) 

Salidas [0, 2, 3, 5, 6, 8, 9] , en lugar de [0, 2, 4, 6, 8] cuando se usa Python 3.2.

  1. ¿Por qué produce estos valores particulares?
  2. ¿Por qué no se da ningún error para indicar que el iterador subyacente se está modificando?
  3. ¿Se ha cambiado la mecánica de las versiones anteriores de Python con respecto a este comportamiento?

Tenga en cuenta que no busco evitar el comportamiento, sino comprenderlo.

Debatí responder esto por un tiempo, porque aquí se han hecho muchas preguntas similares. Pero es lo suficientemente único como para recibir el beneficio de la duda. (Aún así, no objetaré si otros votan para cerrar). Aquí hay una explicación visual de lo que está sucediendo.

 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] <- b = 0; remove? no ^ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] <- b = 1; remove? yes ^ [0, 2, 3, 4, 5, 6, 7, 8, 9] <- b = 3; remove? no ^ [0, 2, 3, 4, 5, 6, 7, 8, 9] <- b = 4; remove? yes ^ [0, 2, 3, 5, 6, 7, 8, 9] <- b = 6; remove? no ^ [0, 2, 3, 5, 6, 7, 8, 9] <- b = 7; remove? yes ^ [0, 2, 3, 5, 6, 8, 9] <- b = 9; remove? no ^ 

Como nadie más lo ha hecho, intentaré responder tus otras preguntas:

¿Por qué no se da ningún error para indicar que el iterador subyacente se está modificando?

Para lanzar un error sin prohibir muchas construcciones de bucle perfectamente válidas, Python tendría que saber mucho sobre lo que está sucediendo, y probablemente tendría que obtener esa información en tiempo de ejecución. Toda esa información llevaría tiempo procesarla. Haría que Python fuera mucho más lento, solo en el lugar donde la velocidad realmente cuenta: un bucle.

¿Se ha cambiado la mecánica de las versiones anteriores de Python con respecto a este comportamiento?

En resumen, no. O al menos lo dudo mucho, y ciertamente se ha comportado de esta manera desde que aprendí Python (2.4). Francamente, esperaría que cualquier implementación directa de una secuencia mutable se comporte de esta manera. Cualquiera que sepa mejor, por favor corríjame. (En realidad, ¡una búsqueda rápida de documentos confirma que el texto que Mikola citó ha estado en el tutorial desde la versión 1.4 !)

Como explicó Mikola, el resultado real que observas se debe al hecho de que al eliminar una entrada de la lista la lista completa se desplaza en un punto, lo que hace que pierdas elementos.

Pero la pregunta más interesante, en mi opinión, es por qué Python no elige producir un mensaje de error cuando esto sucede. Produce dicho mensaje de error si intenta modificar un diccionario. Creo que hay dos razones para eso.

  1. Los dictados son complejos internamente, mientras que las listas no lo son. Las listas son básicamente matrices. Un dict tiene que detectar cuándo se modifica mientras se está iterando para evitar que se bloquee cuando cambia la estructura interna del dict. Una lista puede escapar sin hacer esa comprobación porque solo se asegura de que su índice actual todavía esté dentro del rango.

  2. Históricamente, (no estoy seguro ahora), las listas de python se iteraron utilizando el operador []. Python evaluaría la lista [0], la lista [1], la lista [2] hasta que obtuviera un IndexError. En ese caso, Python no rastreaba el tamaño de la lista antes de que comenzara, por lo que no tenía ningún método para detectar que el tamaño de la lista había cambiado.

Por supuesto, no es seguro modificar una matriz cuando se está iterando sobre ella. La especificación dice que es una mala idea y el comportamiento no está definido:

http://docs.python.org/tutorial/controlflow.html#for-statements

Entonces, la siguiente pregunta es ¿qué está pasando exactamente debajo del capó aquí? Si tuviera que adivinar, diría que está haciendo algo como esto:

 for(int i=0; i 

Si supone que esto es realmente lo que está sucediendo, entonces se explica completamente el comportamiento observado. Cuando elimina un elemento en o antes del puntero actual, desplaza la lista completa 1 hacia la izquierda. La primera vez, eliminas un 1, como siempre, pero ahora la lista se desplaza hacia atrás. En la siguiente iteración, en lugar de golpear un 2, golpeas un 3. Luego eliminas un 4, y la lista se desplaza hacia atrás. Siguiente iteración 7, y así sucesivamente.

En tu primera iteración, no estás eliminando y todo está bien.

En la segunda iteración, se encuentra en la posición [1] de la secuencia y se elimina ‘1’. El iterador lo lleva a la posición [2] en la secuencia, que ahora es ‘3’, por lo que se omite ‘2’ (ya que ‘2’ está ahora en la posición [1] debido a la eliminación). Por supuesto, ‘3’ no se elimina, por lo que pasa a la posición [3] en la secuencia, que ahora es ‘4’. Eso se elimina, llevándote a la posición [5] que ahora es ‘6’, y así sucesivamente.

El hecho de que esté eliminando cosas significa que una posición se omite cada vez que realiza una eliminación.