Ändern eines Python-Diktats beim Durchlaufen

85

Nehmen wir an, wir haben ein Python-Wörterbuch dund iterieren wie folgt darüber:

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

( fund gsind nur einige Black-Box-Transformationen.)

Mit anderen Worten, wir versuchen, Elemente hinzuzufügen / zu entfernen, dwährend wir mit darüber iterieren iteritems.

Ist das gut definiert? Könnten Sie einige Referenzen angeben, um Ihre Antwort zu unterstützen?

(Es ist ziemlich offensichtlich, wie man das behebt, wenn es kaputt ist, also ist dies nicht der Winkel, nach dem ich suche.)

NPE
quelle
Ich habe versucht, dies zu tun, und es scheint, dass, wenn Sie die anfängliche Diktatgröße unverändert lassen - z. B. einen Schlüssel / Wert ersetzen, anstatt sie zu entfernen, dieser Code keine Ausnahme
auslöst
Ich bin nicht der Meinung, dass es für alle, die nach diesem Thema suchen (einschließlich mir), „ziemlich offensichtlich ist, wie man das behebt, wenn es kaputt ist“, und ich wünschte, die akzeptierte Antwort hätte dies zumindest berührt.
Alex Peters

Antworten:

52

Auf der Python-Dokumentseite (für Python 2.7 ) wird ausdrücklich darauf hingewiesen

Die Verwendung iteritems()beim Hinzufügen oder Löschen von Einträgen im Wörterbuch kann dazu führen, dass RuntimeErroralle Einträge nicht wiederholt werden.

Ähnliches gilt für Python 3 .

Das gleiche gilt für iter(d), d.iterkeys()und d.itervalues(), und ich werde so weit gehen , wie zu sagen , dass es funktioniert für for k, v in d.items():(ich kann nicht genau erinnern , was der forFall ist, aber ich würde nicht überrascht sein , wenn die Umsetzung genannt iter(d)).

Raphaël Saint-Pierre
quelle
44
Ich werde mich für die Community in Verlegenheit bringen, indem ich erkläre, dass ich genau das Code-Snippet verwendet habe. Da ich keinen RuntimeError bekam, dachte ich, dass alles gut war. Und es war für eine Weile. Anal-remanente Unit-Tests gaben mir die Daumen hoch und es lief sogar gut, als es veröffentlicht wurde. Dann bekam ich bizarres Verhalten. Was geschah, war, dass Elemente im Wörterbuch übersprungen wurden und daher nicht alle Elemente im Wörterbuch gescannt wurden. Kinder, lernen Sie aus den Fehlern, die ich in meinem Leben gemacht habe, und sagen Sie einfach nein! ;)
Alan Cabrera
2
Kann ich auf Probleme stoßen, wenn ich den Wert am aktuellen Schlüssel ändere (aber keine Schlüssel hinzufüge oder entferne?)? Ich würde mir vorstellen, dass dies keine Probleme verursachen sollte, aber ich würde es gerne wissen!
Gershom
@GershomMaes Ich kenne keine, aber Sie stoßen möglicherweise immer noch auf ein Minenfeld, wenn Ihr Schleifenkörper den Wert verwendet und nicht erwartet, dass er sich ändert.
Raphaël Saint-Pierre
3
d.items()sollte in Python 2.7 sicher sein (das Spiel ändert sich mit Python 3), da es im Wesentlichen eine Kopie davon erstellt d, sodass Sie nicht ändern, worüber Sie iterieren.
Paul Price
Es wäre interessant zu wissen, ob dies auch fürviewitems()
jlh
49

Alex Martelli spricht hier darüber .

Es ist möglicherweise nicht sicher, den Container zu wechseln (z. B. diktieren), während Sie den Container durchlaufen. So del d[f(k)]kann nicht sicher sein. Wie Sie wissen, besteht die Problemumgehung darin, d.items()(um eine unabhängige Kopie des Containers zu durchlaufen) anstelle von d.iteritems()(die denselben zugrunde liegenden Container verwendet) zu verwenden.

Es ist in Ordnung, den Wert an einem vorhandenen Index des Diktats zu ändern , aber das Einfügen von Werten an neuen Indizes (z. B. d[g(k)]=v) funktioniert möglicherweise nicht.

unutbu
quelle
3
Ich denke, das ist eine wichtige Antwort für mich. In vielen Anwendungsfällen werden Dinge durch einen Prozess eingefügt und durch einen anderen bereinigt / gelöscht, sodass der Rat zur Verwendung von d.items () funktioniert. Python 3 Vorbehalte nicht standhalten
easytiger
4
Weitere Informationen zu den Python 3-Vorbehalten finden Sie in PEP 469, wo die semantischen Äquivalente der oben genannten Python 2-Diktiermethoden aufgelistet sind.
Lionel Brooks
1
"Es ist in Ordnung, den Wert an einem vorhandenen Index des Diktats zu ändern" - haben Sie eine Referenz dafür?
Jonathon Reinhart
1
@ JonathonReinhart: Nein, ich habe keine Referenz dafür, aber ich denke, dass es in Python ziemlich Standard ist. Zum Beispiel war Alex Martelli ein Python-Kernentwickler und demonstriert hier seine Verwendung .
Unutbu
25

Das kannst du zumindest nicht mit d.iteritems(). Ich habe es versucht und Python schlägt fehl mit

RuntimeError: dictionary changed size during iteration

Wenn Sie stattdessen verwenden d.items(), funktioniert es.

In Python 3 gibt d.items()es eine Ansicht in das Wörterbuch, wie d.iteritems()in Python 2. Verwenden Sie dazu stattdessen Python 3 d.copy().items(). Auf diese Weise können wir auch eine Kopie des Wörterbuchs durchlaufen, um zu vermeiden, dass die Datenstruktur, über die wir iterieren, geändert wird.

murgatroid99
quelle
2
Ich habe meiner Antwort Python 3 hinzugefügt.
Murgatroid99
2
2to3Zu Ihrer d.items()Information , die wörtliche Übersetzung (wie z. B. von ) von Py2 in Py3 ist list(d.items()), obwohl sie d.copy().items()wahrscheinlich von vergleichbarer Effizienz ist.
Søren Løvborg
2
Wenn das diktierte Objekt sehr groß ist, ist d.copy (). Items () wirksam?
Libelle
11

Ich habe ein großes Wörterbuch mit Numpy-Arrays, daher war das von @ murgatroid99 vorgeschlagene dict.copy (). Keys () -Ding nicht durchführbar (obwohl es funktioniert hat). Stattdessen habe ich die keys_view einfach in eine Liste konvertiert und es hat gut funktioniert (in Python 3.4):

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

Mir ist klar, dass dies nicht wie die obigen Antworten in den philosophischen Bereich von Pythons Innenleben eintaucht, aber es bietet eine praktische Lösung für das angegebene Problem.

2cynykyl
quelle
6

Der folgende Code zeigt, dass dies nicht genau definiert ist:

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

Das erste Beispiel ruft g (k) auf und löst eine Ausnahme aus (die Größe des Wörterbuchs wurde während der Iteration geändert).

Das zweite Beispiel ruft h (k) auf und löst keine Ausnahme aus, sondern gibt Folgendes aus:

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

Was beim Betrachten des Codes falsch erscheint - ich hätte so etwas erwartet:

{11: 'ax', 12: 'bx', 13: 'cx'}
Kampfdave
quelle
Ich kann verstehen, warum Sie es erwarten könnten, {11: 'ax', 12: 'bx', 13: 'cx'}aber der 21,22,23 sollte Ihnen einen Hinweis darauf geben, was tatsächlich passiert ist: Ihre Schleife hat die Punkte 1, 2, 3, 11, 12, 13 durchlaufen, aber den zweiten nicht aufgenommen Runde neuer Elemente, wenn sie vor den Elementen eingefügt wurden, über die Sie bereits iteriert hatten. Wechseln Sie h()zu Rückkehr x+5und Sie erhalten ein weiteres x: 'axxx'usw. oder 'x + 3' und Sie erhalten das großartige'axxxxx'
Duncan
Ja, ich fürchte, mein Fehler - meine erwartete Ausgabe war {11: 'ax', 12: 'bx', 13: 'cx'}wie Sie sagten, also werde ich meinen Beitrag darüber aktualisieren. In beiden Fällen ist dies eindeutig kein genau definiertes Verhalten.
Combatdave
1

Ich habe das gleiche Problem und habe das folgende Verfahren verwendet, um dieses Problem zu lösen.

Python-Liste kann iteriert werden, auch wenn Sie sie während der Iteration ändern. Wenn Sie also dem folgenden Code folgen, werden 1 unendlich gedruckt.

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

Wenn Sie also list und dict gemeinsam verwenden, können Sie dieses Problem lösen.

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
quelle
Ich bin nicht sicher, ob es sicher ist, eine Liste während der Iteration zu ändern (obwohl dies in einigen Fällen funktionieren kann). Siehe diese Frage zum Beispiel ...
Roman
@Roman Wenn Sie Elemente einer Liste löschen möchten, können Sie diese sicher in umgekehrter Reihenfolge durchlaufen, da sich der Index des nächsten Elements in normaler Reihenfolge beim Löschen ändern würde. Siehe dieses Beispiel.
mbomb007
1

Python 3 sollten Sie nur:

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

oder verwenden:

t2 = t1.copy()

Sie sollten das Originalwörterbuch niemals ändern, da dies zu Verwirrung sowie potenziellen Fehlern oder RunTimeErrors führt. Es sei denn, Sie fügen dem Wörterbuch nur neue Schlüsselnamen hinzu.

Dexter
quelle
0

Heute hatte ich einen ähnlichen Anwendungsfall, aber anstatt einfach die Schlüssel im Wörterbuch am Anfang der Schleife zu materialisieren, wollte ich Änderungen am Diktat, um die Iteration des Diktats zu beeinflussen, das ein geordnetes Diktat war.

Am Ende habe ich die folgende Routine erstellt, die auch in jaraco.itertools zu finden ist :

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)

Die Dokumentzeichenfolge veranschaulicht die Verwendung. Diese Funktion könnte anstelle der d.iteritems()oben genannten verwendet werden, um den gewünschten Effekt zu erzielen.

Jason R. Coombs
quelle