¿Cómo eliminar elementos de un diccionario mientras itera sobre él?

295

¿Es legítimo eliminar elementos de un diccionario en Python mientras itera sobre él?

Por ejemplo:

for k, v in mydict.iteritems():
   if k == val:
     del mydict[k]

La idea es eliminar elementos del diccionario que no cumplan una determinada condición, en lugar de crear un nuevo diccionario que sea un subconjunto del que se repite.

¿Es esta una buena solución? ¿Hay formas más elegantes / eficientes?

Trilarion
fuente
1
Una pregunta relacionada con respuestas muy interesantes: stackoverflow.com/questions/9023078/… .
max
Se podría haber intentado fácilmente. Si falla, no es legítimo.
Trilarion
26
@Trilarion Uno podría haberlo intentado fácilmente ... y fácilmente no haber aprendido nada de valor. Si tiene éxito, es no necesariamente legítimo. Abundan los casos extremos y las advertencias inesperadas. Esta pregunta no es de interés trivial para todos los aspirantes a pitonistas. Despido con la mano en el orden de "¡Uno podría haberlo intentado fácilmente!" es inútil y contrario al espíritu inquisitivo de la investigación de stackoverflow.
Cecil Curry
Después de leer la pregunta relacionada con max , debo estar de acuerdo. Probablemente solo quiera leer esa pregunta inquietantemente profunda y sus respuestas bien escritas. Tu mente pitónica se volará.
Cecil Curry
1
@CecilCurry Probar una idea antes de presentarla aquí es algo así como el espíritu de stackoverflow si no me equivoco. Eso fue todo lo que quería transmitir. Lo siento si ha habido alguna perturbación por eso. También creo que es una buena pregunta y no la rechacé. Me gusta más la respuesta de Jochen Ritzel . No creo que sea necesario hacer todo eso para eliminar sobre la marcha cuando eliminar en un segundo paso es mucho más simple. Esa debería ser la forma preferida en mi opinión.
Trilarion

Respuestas:

305

EDITAR:

Esta respuesta no funcionará para Python3 y dará un RuntimeError.

RuntimeError: el diccionario cambió de tamaño durante la iteración.

Esto sucede porque mydict.keys()devuelve un iterador, no una lista. Como se señaló en los comentarios, simplemente conviértalo mydict.keys()en una lista list(mydict.keys())y debería funcionar.


Una prueba simple en la consola muestra que no puede modificar un diccionario mientras itera sobre él:

>>> mydict = {'one': 1, 'two': 2, 'three': 3, 'four': 4}
>>> for k, v in mydict.iteritems():
...    if k == 'two':
...        del mydict[k]
...
------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython console>", line 1, in <module>
RuntimeError: dictionary changed size during iteration

Como se indica en la respuesta de delnan, la eliminación de entradas causa problemas cuando el iterador intenta pasar a la siguiente entrada. En su lugar, utilice el keys()método para obtener una lista de las claves y trabaje con eso:

>>> for k in mydict.keys():
...    if k == 'two':
...        del mydict[k]
...
>>> mydict
{'four': 4, 'three': 3, 'one': 1}

Si necesita eliminar según el valor de los elementos, utilice el items()método en su lugar:

>>> for k, v in mydict.items():
...     if v == 3:
...         del mydict[k]
...
>>> mydict
{'four': 4, 'one': 1}
Blair
fuente
53
Tenga en cuenta que en Python 3, dict.items () devuelve un iterador (y dict.iteritems () se ha ido).
Tim Lesher
83
Para elaborar el comentario de @TimLesher ... Esto NO funcionará en Python 3.
max
99
Para elaborar sobre la elaboración de @ max, funcionará si convierte el código anterior con 2to3. Uno de los correctores predeterminados hará que el bucle se vea como lo for k, v in list(mydict.items()):que funciona bien en Python 3. Lo mismo para keys()convertirse list(keys()).
Walter Mundt
8
Esto no funciona Me sale un error:RuntimeError: dictionary changed size during iteration
Tomáš Zato - Restablece a Mónica el
15
@ TomášZato, como señaló Walter, para python3 debe usarlo, for k in list(mydict.keys()): ya que python3 hace que el método keys () sea un iterador, y también no permite eliminar elementos dict durante la iteración. Al agregar una llamada list (), convierte el iterador keys () en una lista. Entonces, cuando estás en el cuerpo del bucle for, ya no estás iterando sobre el diccionario.
Geoff Crompton
89

También puedes hacerlo en dos pasos:

remove = [k for k in mydict if k == val]
for k in remove: del mydict[k]

Mi enfoque favorito suele ser simplemente hacer un nuevo dict:

# Python 2.7 and 3.x
mydict = { k:v for k,v in mydict.items() if k!=val }
# before Python 2.7
mydict = dict((k,v) for k,v in mydict.iteritems() if k!=val)
Jochen Ritzel
fuente
11
@senderle: desde 2.7 en realidad.
Jochen Ritzel
55
El enfoque de comprensión dict hace una copia del diccionario; Afortunadamente, los valores al menos no se copian en profundidad, solo se vinculan. Aún así, si tienes muchas llaves, podría ser malo. Por esa razón, me gusta más el removeenfoque de bucle.
max
1
También puede combinar los pasos:for k in [k for k in mydict if k == val]: del mydict[k]
AXO
la primera solución es la única eficiente en grandes dictos en este hilo hasta ahora, ya que no hace una copia completa.
kxr
21

No puede modificar una colección mientras la itera. Esa forma es una locura: sobre todo, si se le permitiera eliminar y eliminar el elemento actual, el iterador tendría que avanzar (+1) y la siguiente llamada a nextlo llevaría más allá de eso (+2), por lo que termina omitiendo un elemento (el que está justo detrás del que eliminó). Tienes dos opciones:

  • Copie todas las claves (o valores, o ambos, dependiendo de lo que necesite), luego repita sobre ellos. Puede usar .keys()et al para esto (en Python 3, pase el iterador resultante a list). Sin embargo, podría ser un gran desperdicio de espacio.
  • Repetir mydictcomo de costumbre, guardando las claves para eliminar en una colección separada to_delete. Cuando termine de iterar mydict, elimine todos los elementos to_deletede mydict. Ahorra algo de espacio (dependiendo de cuántas claves se eliminen y cuántas permanecen) durante el primer enfoque, pero también requiere algunas líneas más.

fuente
You can't modify a collection while iterating it.esto es correcto para dictados y amigos, pero puede modificar las listas durante la iteración:L = [1,2,None,4,5] <\n> for n,x in enumerate(L): <\n\t> if x is None: del L[n]
Nils Lindemann
3
@Nils No arroja una excepción, pero sigue siendo incorrecta. Observe: codepad.org/Yz7rjDVT - vea, por ejemplo, stackoverflow.com/q/6260089/395760 para obtener una explicación
Me tienes aquí. Todavía can'tes correcto solo para dict y amigos, mientras que debería ser shouldn'tpara listas.
Nils Lindemann
21

En su lugar, repite una copia, como la que devuelve items():

for k, v in list(mydict.items()):
Ignacio Vazquez-Abrams
fuente
1
Eso no tiene mucho sentido, entonces no puedes hacerlo del vdirectamente, por lo que has hecho una copia de cada v que nunca vas a usar y tienes que acceder a los elementos por clave de todos modos. dict.keys()Es una mejor opción.
jscs
2
@ Josh: Todo depende de cuánto necesitará usar vcomo criterio para la eliminación.
Ignacio Vazquez-Abrams
3
En Python 3, dict.items()devuelve un iterador en lugar de una copia. Vea el comentario para la respuesta de Blair , que (tristemente) también asume la semántica de Python 2.
Cecil Curry
11

Es más limpio de usar list(mydict):

>>> mydict = {'one': 1, 'two': 2, 'three': 3, 'four': 4}
>>> for k in list(mydict):
...     if k == 'three':
...         del mydict[k]
... 
>>> mydict
{'four': 4, 'two': 2, 'one': 1}

Esto corresponde a una estructura paralela para listas:

>>> mylist = ['one', 'two', 'three', 'four']
>>> for k in list(mylist):                            # or mylist[:]
...     if k == 'three':
...         mylist.remove(k)
... 
>>> mylist
['one', 'two', 'four']

Ambos funcionan en python2 y python3.

rsanden
fuente
Esto no es bueno en caso de que su conjunto de datos sea grande. Esto es copiar todos los objetos en la memoria, ¿verdad?
AFP_555
1
@ AFP_555 Sí, mi objetivo aquí es un código limpio, paralelo y pitónico. Si necesita eficiencia de memoria, el mejor enfoque que conozco es iterar y crear una lista de claves para eliminar o una nueva división de elementos para guardar. La belleza es mi prioridad con Python; para grandes conjuntos de datos estoy usando Go u Rust.
rsanden
9

Puedes usar un diccionario de comprensión.

d = {k:d[k] for k in d if d[k] != val}

Aaron
fuente
Este es el más pitónico.
Yehosef
Pero crea un nuevo diccionario en lugar de modificarlo den su lugar.
Aristide
9

Con python3, iterar en dic.keys () elevará el error de tamaño del diccionario. Puede usar esta forma alternativa:

Probado con python3, funciona bien y el error "el diccionario cambió de tamaño durante la iteración " no aparece:

my_dic = { 1:10, 2:20, 3:30 }
# Is important here to cast because ".keys()" method returns a dict_keys object.
key_list = list( my_dic.keys() )

# Iterate on the list:
for k in key_list:
    print(key_list)
    print(my_dic)
    del( my_dic[k] )


print( my_dic )
# {}
glihm
fuente
4

Primero puede crear una lista de claves para eliminar y luego iterar sobre esa lista eliminándolas.

dict = {'one' : 1, 'two' : 2, 'three' : 3, 'four' : 4}
delete = []
for k,v in dict.items():
    if v%2 == 1:
        delete.append(k)
for i in delete:
    del dict[i]
Pob
fuente
Es más bien un duplicado de la primera solución de @Ritzel (eficiente en grandes dictados sin copia completa). Aunque una "lectura larga" sin comprensión de la lista. ¿Sin embargo, es posiblemente más rápido?
kxr
3

Hay una manera que puede ser adecuada si los elementos que desea eliminar siempre están al "comienzo" de la iteración dict

while mydict:
    key, value = next(iter(mydict.items()))
    if should_delete(key, value):
       del mydict[key]
    else:
       break

El "comienzo" solo se garantiza que sea consistente para ciertas versiones / implementaciones de Python. Por ejemplo de What's New In Python 3.7

Se ha declarado que la naturaleza de preservación del orden de inserción de los objetos dict es una parte oficial de la especificación del lenguaje Python.

De esta forma, se evita una copia del dict que sugieren muchas de las otras respuestas, al menos en Python 3.

Michal Charemza
fuente
1

Probé las soluciones anteriores en Python3, pero esta parece ser la única que funciona para mí al almacenar objetos en un dict. Básicamente, hace una copia de su dict () e itera sobre eso mientras elimina las entradas en su diccionario original.

        tmpDict = realDict.copy()
        for key, value in tmpDict.items():
            if value:
                del(realDict[key])
JasonLandbridge
fuente