Modificare un dict di Python durante l'iterazione su di esso


87

Diciamo di avere un dizionario Python de lo stiamo iterando in questo modo:

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

( fe gsono solo alcune trasformazioni della scatola nera.)

In altre parole, proviamo ad aggiungere / rimuovere elementi a dmentre iteriamo su di esso utilizzando iteritems.

È ben definito? Potresti fornire alcuni riferimenti per supportare la tua risposta?

(È abbastanza ovvio come risolvere questo problema se è rotto, quindi questo non è l'angolo che cerco.)




Ho provato a farlo e sembra che se lasci invariata la dimensione iniziale del dict, ad esempio sostituisci qualsiasi chiave / valore invece di rimuoverli, questo codice non genererà eccezioni
Artsiom Rudzenka

Non sono d'accordo sul fatto che sia "abbastanza ovvio come risolvere questo problema se è rotto" per tutti coloro che cercano questo argomento (me compreso), e vorrei che la risposta accettata avesse almeno toccato questo.
Alex Peters,

Risposte:


54

È esplicitamente menzionato nella pagina della documentazione di Python (per Python 2.7 ) che

L'utilizzo iteritems()durante l'aggiunta o l'eliminazione di voci nel dizionario può generare a RuntimeErroro non riuscire a eseguire l'iterazione su tutte le voci.

Allo stesso modo per Python 3 .

Lo stesso vale per iter(d), d.iterkeys()e d.itervalues(), e mi spingerò a dire che lo fa per for k, v in d.items():(non ricordo esattamente cosa forfa, ma non sarei sorpreso se l'implementazione chiamasse iter(d)).


51
Mi metterò in imbarazzo per il bene della comunità affermando che ho usato lo snippet di codice. Pensando che dal momento che non ho ricevuto un RuntimeError ho pensato che fosse tutto a posto. E lo è stato, per un po '. I test unitari a ritenzione analoga mi stavano dando il pollice in su e funzionava anche bene quando è stato rilasciato. Poi, ho iniziato a comportarmi in modo bizzarro. Quello che stava succedendo era che gli elementi nel dizionario venivano ignorati e quindi non tutti gli elementi nel dizionario venivano scansionati. Ragazzi, imparate dagli errori che ho fatto nella mia vita e dite semplicemente di no! ;)
Alan Cabrera

3
Posso incorrere in problemi se sto cambiando il valore nella chiave corrente (ma non aggiungo o rimuovo nessuna chiave?) Imaaaagine che questo non dovrebbe causare problemi, ma mi piacerebbe saperlo!
Gershy

@GershomMaes Non ne conosco nessuno, ma potresti ancora imbatterti in un campo minato se il tuo corpo del ciclo utilizza il valore e non ti aspetti che cambi.
Raphaël Saint-Pierre,

3
d.items()dovrebbe essere sicuro in Python 2.7 (il gioco cambia con Python 3), poiché crea ciò che è essenzialmente una copia d, quindi non stai modificando ciò su cui stai iterando.
Paul Price

Sarebbe interessante sapere se questo è vero anche perviewitems()
jlh

51

Alex Martelli pesa su questo qui .

Potrebbe non essere sicuro cambiare il contenitore (es. Dict) durante il ciclo del contenitore. Quindi del d[f(k)]potrebbe non essere sicuro. Come sapete, la soluzione alternativa consiste nell'usare d.items()(per eseguire il ciclo su una copia indipendente del contenitore) anziché d.iteritems()(che utilizza lo stesso contenitore sottostante).

Va bene modificare il valore in un indice esistente del dict, ma l'inserimento di valori in nuovi indici (ad esempio d[g(k)]=v) potrebbe non funzionare.


3
Penso che questa sia una risposta chiave per me. Molti casi d'uso avranno un processo per inserire oggetti e un altro per pulirli / cancellarli, quindi il consiglio di usare d.items () funziona. Gli avvertimenti di Python 3 non resistono
easytiger

4
Maggiori informazioni sugli avvertimenti di Python 3 possono essere trovate in PEP 469 dove sono enumerati gli equivalenti semantici dei suddetti metodi dict di Python 2.
Lionel Brooks

1
"Va bene modificare il valore in un indice esistente del dict" - hai un riferimento per questo?
Jonathon Reinhart,

1
@JonathonReinhart: No, non ho un riferimento per questo, ma penso che sia abbastanza standard in Python. Ad esempio, Alex Martelli era uno sviluppatore principale di Python e qui ne dimostra l'utilizzo .
unutbu

27

Non puoi farlo, almeno con d.iteritems(). L'ho provato e Python non riesce con

RuntimeError: dictionary changed size during iteration

Se invece usi d.items(), allora funziona.

In Python 3, d.items()è una vista nel dizionario, come d.iteritems()in Python 2. Per farlo in Python 3, usa invece d.copy().items(). Allo stesso modo questo ci permetterà di iterare su una copia del dizionario per evitare di modificare la struttura dei dati su cui stiamo iterando.


2
Ho aggiunto Python 3 alla mia risposta.
murgatroid99

2
Cordiali saluti, la traduzione letterale (come ad esempio usata da 2to3) di Py2 d.items()in Py3 è list(d.items()), sebbene d.copy().items()sia probabilmente di efficienza comparabile.
Søren Løvborg

2
Se l'oggetto dict è molto grande, d.copy (). Items () è efficiente?
libellula

12

Ho un grande dizionario contenente array Numpy, quindi la cosa dict.copy (). Keys () suggerita da @ murgatroid99 non era fattibile (sebbene funzionasse). Invece, ho appena convertito keys_view in un elenco e ha funzionato bene (in Python 3.4):

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

Mi rendo conto che questo non si immerge nel regno filosofico del funzionamento interno di Python come le risposte sopra, ma fornisce una soluzione pratica al problema dichiarato.


6

Il codice seguente mostra che questo non è ben definito:

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

Il primo esempio chiama g (k) e genera un'eccezione (la dimensione del dizionario cambia durante l'iterazione).

Il secondo esempio chiama h (k) e non genera eccezioni, ma restituisce:

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

Il che, guardando il codice, sembra sbagliato - mi sarei aspettato qualcosa del tipo:

{11: 'ax', 12: 'bx', 13: 'cx'}

Posso capire perché potresti aspettarti, {11: 'ax', 12: 'bx', 13: 'cx'}ma il 21,22,23 dovrebbe darti un indizio su cosa è realmente accaduto: il tuo ciclo ha attraversato gli elementi 1, 2, 3, 11, 12, 13 ma non è riuscito a riprendere il secondo round di nuovi elementi man mano che venivano inseriti davanti agli elementi su cui avevi già ripetuto. Cambia h()per tornare x+5e ottieni un'altra x: 'axxx'ecc. O 'x + 3' e ottieni il magnifico'axxxxx'
Duncan

Sì, il mio errore temo: il mio output previsto è stato {11: 'ax', 12: 'bx', 13: 'cx'}come hai detto, quindi aggiornerò il mio post a riguardo. In ogni caso, questo è un comportamento chiaramente non ben definito.
combatdave

1

Ho avuto lo stesso problema e ho usato la seguente procedura per risolvere questo problema.

Python List può essere iterato anche se lo modifichi durante l'iterazione su di esso. quindi per il codice successivo stamperà gli 1 all'infinito.

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

Quindi usando list e dict in modo collaborativo puoi risolvere questo 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))

Non sono sicuro che sia sicuro modificare un elenco durante l'iterazione (anche se in alcuni casi potrebbe funzionare). Vedi questa domanda per esempio ...
Roman

@Roman Se vuoi cancellare gli elementi di una lista, puoi tranquillamente iterare su di essa in ordine inverso, poiché nell'ordine normale l'indice dell'elemento successivo cambierebbe dopo la cancellazione. Vedi questo esempio.
mbomb007

1

Python 3 dovresti solo:

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

oppure usa:

t2 = t1.copy()

Non dovresti mai modificare il dizionario originale, porta a confusione e potenziali bug o RunTimeErrors. A meno che non si aggiunga al dizionario solo nuovi nomi di chiavi.


0

Oggi ho avuto un caso d'uso simile, ma invece di materializzare semplicemente le chiavi sul dizionario all'inizio del ciclo, volevo che le modifiche al dict influenzassero l'iterazione del dict, che era un dict ordinato.

Ho finito per creare la seguente routine, che può essere trovata anche in 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 docstring illustra l'utilizzo. Questa funzione potrebbe essere utilizzata al posto della d.iteritems()precedente per ottenere l'effetto desiderato.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.