Modificar un dictado de Python mientras se itera sobre él

87

Digamos que tenemos un diccionario de Python dy lo estamos repitiendo así:

for k,v in d.iteritems():
    del d[f(k)] # remove some item
    d[g(k)] = v # add a new item

( fy gson solo algunas transformaciones de caja negra).

En otras palabras, intentamos agregar / eliminar elementos dmientras iteramos sobre él usando iteritems.

¿Está esto bien definido? ¿Podría proporcionar algunas referencias para respaldar su respuesta?

(Es bastante obvio cómo arreglar esto si está roto, por lo que este no es el ángulo que busco).

NPE
fuente
Intenté hacer esto y parece que si dejas el tamaño de dictado inicial sin cambios, por ejemplo, reemplaza cualquier clave / valor en lugar de eliminarlos, entonces este código no arrojará una excepción
Artsiom Rudzenka
No estoy de acuerdo en que sea "bastante obvio cómo solucionar este problema si no funciona" para todos los que busquen este tema (incluyéndome a mí), y desearía que la respuesta aceptada al menos hubiera tocado esto.
Alex Peters

Respuestas:

53

Se menciona explícitamente en la página del documento de Python (para Python 2.7 ) que

El uso iteritems()al agregar o eliminar entradas en el diccionario puede generar un RuntimeErrorerror o no iterar sobre todas las entradas.

De manera similar para Python 3 .

Lo mismo vale para iter(d), d.iterkeys()y d.itervalues(), iré tan lejos como para decir que lo hace for k, v in d.items():(no recuerdo exactamente qué fores lo que hace, pero no me sorprendería si la implementación llamara iter(d)).

Raphaël Saint-Pierre
fuente
48
Me avergonzaré por el bien de la comunidad al afirmar que utilicé el mismo fragmento de código. Pensando que como no obtuve un RuntimeError pensé que todo estaba bien. Y lo fue, por un tiempo. Las pruebas unitarias analmente retentivas me estaban dando el visto bueno e incluso funcionaba bien cuando se lanzó. Entonces, comencé a tener un comportamiento extraño. Lo que estaba sucediendo era que los elementos del diccionario se saltaban y, por lo tanto, no se escaneaban todos los elementos del diccionario. Niños, aprendan de los errores que he cometido en mi vida y simplemente digan que no. ;)
Alan Cabrera
3
¿Puedo tener problemas si estoy cambiando el valor en la clave actual (pero no agrego ni elimino ninguna clave?) Me imagino que esto no debería causar ningún problema, ¡pero me gustaría saberlo!
Gershom
@GershomMaes No conozco ninguno, pero es posible que aún se encuentre con un campo minado si su cuerpo de bucle hace uso del valor y no espera que cambie.
Raphaël Saint-Pierre
3
d.items()debería ser seguro en Python 2.7 (el juego cambia con Python 3), ya que hace lo que es esencialmente una copia de d, por lo que no está modificando lo que está iterando.
Paul Price
Sería interesante saber si esto también es cierto paraviewitems()
jlh
50

Alex Martelli interviene en esto aquí .

Puede que no sea seguro cambiar el contenedor (por ejemplo, dict) mientras se coloca sobre el contenedor. Así que del d[f(k)]puede que no sea seguro. Como sabe, la solución es usar d.items()(para recorrer una copia independiente del contenedor) en lugar de d.iteritems()(que usa el mismo contenedor subyacente).

Está bien modificar el valor en un índice existente del dict, pero es posible que la inserción de valores en índices nuevos (por ejemplo d[g(k)]=v) no funcione.

unutbu
fuente
3
Creo que esta es una respuesta clave para mí. Muchos casos de uso tendrán un proceso insertando cosas y otro limpiando cosas / borrándolas, por lo que el consejo para usar d.items () funciona. No resisten las advertencias de Python 3
easytiger
4
Se puede encontrar más información sobre las advertencias de Python 3 en PEP 469, donde se enumeran los equivalentes semánticos de los métodos de dictado de Python 2 antes mencionados.
Lionel Brooks
1
"Está bien modificar el valor en un índice existente del dict" . ¿Tiene una referencia para esto?
Jonathon Reinhart
1
@JonathonReinhart: No, no tengo una referencia para esto, pero creo que es bastante estándar en Python. Por ejemplo, Alex Martelli era un desarrollador central de Python y demuestra su uso aquí .
unutbu
27

No puedes hacer eso, al menos con d.iteritems(). Lo probé y Python falla con

RuntimeError: dictionary changed size during iteration

Si en cambio lo usa d.items(), entonces funciona.

En Python 3, d.items()es una vista del diccionario, como d.iteritems()en Python 2. Para hacer esto en Python 3, en su lugar use d.copy().items(). De manera similar, esto nos permitirá iterar sobre una copia del diccionario para evitar modificar la estructura de datos sobre la que estamos iterando.

murgatroid99
fuente
2
Agregué Python 3 a mi respuesta.
murgatroid99
2
Para su información, la traducción literal (como por ejemplo utilizada por 2to3) de Py2 d.items()a Py3 es list(d.items()), aunque d.copy().items()probablemente sea de eficiencia comparable.
Søren Løvborg
2
Si el objeto dict es muy grande, ¿d.copy (). Items () es eficiente?
libélula
11

Tengo un diccionario grande que contiene matrices Numpy, por lo que lo dict.copy (). Keys () sugerido por @ murgatroid99 no fue factible (aunque funcionó). En cambio, acabo de convertir keys_view en una lista y funcionó bien (en Python 3.4):

for item in list(dict_d.keys()):
    temp = dict_d.pop(item)
    dict_d['some_key'] = 1  # Some value

Me doy cuenta de que esto no se sumerge en el ámbito filosófico del funcionamiento interno de Python como las respuestas anteriores, pero proporciona una solución práctica al problema planteado.

2cynykyl
fuente
6

El siguiente código muestra que esto no está bien definido:

def f(x):
    return x

def g(x):
    return x+1

def h(x):
    return x+10

try:
    d = {1:"a", 2:"b", 3:"c"}
    for k, v in d.iteritems():
        del d[f(k)]
        d[g(k)] = v+"x"
    print d
except Exception as e:
    print "Exception:", e

try:
    d = {1:"a", 2:"b", 3:"c"}
    for k, v in d.iteritems():
        del d[f(k)]
        d[h(k)] = v+"x"
    print d
except Exception as e:
    print "Exception:", e

El primer ejemplo llama a g (k) y arroja una excepción (el tamaño del diccionario cambió durante la iteración).

El segundo ejemplo llama a h (k) y no arroja ninguna excepción, pero genera:

{21: 'axx', 22: 'bxx', 23: 'cxx'}

Lo cual, mirando el código, parece incorrecto, habría esperado algo como:

{11: 'ax', 12: 'bx', 13: 'cx'}
combatdave
fuente
Puedo entender por qué podría esperar, {11: 'ax', 12: 'bx', 13: 'cx'}pero el 21,22,23 debería darle una pista de lo que realmente sucedió: su ciclo pasó por los elementos 1, 2, 3, 11, 12, 13 pero no logró recoger el segundo ronda de elementos nuevos a medida que se insertaron delante de los elementos que ya había iterado. Cambie h()para regresar x+5y obtendrá otra x: 'axxx'etc. o 'x + 3' y obtendrá el magnífico'axxxxx'
Duncan
Sí, me temo que fue mi error: mi resultado esperado fue el {11: 'ax', 12: 'bx', 13: 'cx'}que dijiste, así que actualizaré mi publicación al respecto. De cualquier manera, este claramente no es un comportamiento bien definido.
combatdave
1

Tengo el mismo problema y utilicé el siguiente procedimiento para resolver este problema.

La lista de Python se puede iterar incluso si la modifica durante la iteración sobre ella. así que para el siguiente código imprimirá 1 infinitamente.

for i in list:
   list.append(1)
   print 1

Entonces, usando list y dict de manera colaborativa, puede resolver este problema.

d_list=[]
 d_dict = {} 
 for k in d_list:
    if d_dict[k] is not -1:
       d_dict[f(k)] = -1 # rather than deleting it mark it with -1 or other value to specify that it will be not considered further(deleted)
       d_dict[g(k)] = v # add a new item 
       d_list.append(g(k))
Zeel Shah
fuente
No estoy seguro de si es seguro modificar una lista durante la iteración (aunque puede funcionar en algunos casos). Vea esta pregunta, por ejemplo ...
Roman
@Roman Si desea eliminar elementos de una lista, puede iterar sobre ella con seguridad en orden inverso, ya que en orden normal el índice del siguiente elemento cambiaría al eliminarse. Vea este ejemplo.
mbomb007
1

Python 3 solo debes:

prefix = 'item_'
t = {'f1': 'ffw', 'f2': 'fca'}
t2 = dict() 
for k,v in t.items():
    t2[k] = prefix + v

o usar:

t2 = t1.copy()

Nunca debe modificar el diccionario original, ya que genera confusión, así como posibles errores o RunTimeErrors. A menos que agregue al diccionario nuevos nombres de clave.

Diestro
fuente
0

Hoy tuve un caso de uso similar, pero en lugar de simplemente materializar las claves en el diccionario al comienzo del ciclo, quería que los cambios en el dictado afectaran la iteración del dictado, que era un dictado ordenado.

Terminé construyendo la siguiente rutina, que también se puede encontrar en jaraco.itertools :

def _mutable_iter(dict):
    """
    Iterate over items in the dict, yielding the first one, but allowing
    it to be mutated during the process.
    >>> d = dict(a=1)
    >>> it = _mutable_iter(d)
    >>> next(it)
    ('a', 1)
    >>> d
    {}
    >>> d.update(b=2)
    >>> list(it)
    [('b', 2)]
    """
    while dict:
        prev_key = next(iter(dict))
        yield prev_key, dict.pop(prev_key)

La cadena de documentos ilustra el uso. Esta función podría utilizarse en lugar de la d.iteritems()anterior para obtener el efecto deseado.

Jason R. Coombs
fuente