Seite 1 von 1

Iterierte Liste manipulieren

Verfasst: Donnerstag 17. Dezember 2009, 16:17
von novoid
Hi!

Ich habe bemerkt, dass wenn ich über eine Liste iteriere und in der Schleife einzelne Elemente per remove() rauslösche, Blödsinn rauskommt.

Vereinfacht sowas wie:

Code: Alles auswählen

for element in liste:
   if element < 42:
      liste.remove(element)
Offensichtlich nimmt es mir Python übel, wenn man die Liste während der for-Schleife manipuliert.

Abgesehen von der Möglichkeit von mapping in diesem simplen Beispiel: wie kann ich das sonst noch erreichen mit zuverlässigem Ergebnis?

Eigentlicher Code:

Code: Alles auswählen

    for row in csvReader:
        ## I know that this got bad performance but it's fast for reasonable number of alarms anyhow
        for alarm in alarms:
            if str(alarm['description']) == row['description'] and \
                str(alarm['begin'])      == row['begin'] and \
                str(alarm['end'])        == row['end'] and \
                str(alarm['alarm'])      == row['alarm']:
                alarm['description']=DELETESTRING
                alarms.remove(alarm)
                break # ein alarm kommt nur 1x vor
-> es bleiben doch tatsächlich (falsche) Listelemente mit DELETESTRING als Wert übrig! Dafür wurden fälschlicherweise andere Elemente gelöscht.

Verfasst: Donnerstag 17. Dezember 2009, 16:27
von EyDu
Hallo!

Am einfachsten ist es, wenn du eine neue Liste erzeugst, welche alle gewünschten Elemente enhält.

Verfasst: Donnerstag 17. Dezember 2009, 16:40
von novoid
EyDu hat geschrieben:Hallo!

Am einfachsten ist es, wenn du eine neue Liste erzeugst, welche alle gewünschten Elemente enhält.
OK. Das kann ich so machen:

Code: Alles auswählen

    csvalarms = []
    for row in csvReader:
        csvalarms.append({'description':row['description'], \
               'begin':row['begin'], 'end':row['end'],'alarm':row['alarm'] })
Das reduziert mein Probem auf: "Wie kann ich zwei Listen vergleichen, um die 'neuen' Elemente herauszufiltern?"

Ergo:
csvalarms = [ a, b, c ]
alarms = [ c, b, d, e]
gesucht ist bei mir nun "new_alarms" mit Wunsch-Ergebnis [ d, e ]

(Hinweis: 'a' aus csvalarms kommt in alarms nicht vor, ist aber auch nicht gesucht!)

Wie macht man sowas? Ich komm einfach auf keinen grünen Zweig ...

Verfasst: Donnerstag 17. Dezember 2009, 16:43
von Dav1d

Code: Alles auswählen

for element in liste[:]:
   if element < 42:
      liste.remove(element) 
Einfach eine Kopie der Liste erzeugen

Verfasst: Donnerstag 17. Dezember 2009, 16:58
von novoid
Dav1d hat geschrieben:Einfach eine Kopie der Liste erzeugen
Dachte ich mir auch:

Code: Alles auswählen

liste_neu = liste_alt
for element in liste_alt:
   if element.eigenschaft = BEDINGUNG:
      liste_neu.remove(element)
Da kommt leider auch der selbe Blödsinn heraus, da Python offenbar in Zeile 2 nur eine Referenz erzeugt. Im Falle einer Kopie wäre ich mir auch nicht sicher gewesen, ob die Referenzierung des Elementes in "liste_neu.remove(element)" auch noch funktionieren würde.

Verfasst: Donnerstag 17. Dezember 2009, 17:15
von cofi
@OP: Das ist keine Kopie! Sondern nur eine weitere Namensbindung.

Um mal `filter` einzuwerfen

Code: Alles auswählen

filter(lambda x: x >= 42, range(100))

Verfasst: Donnerstag 17. Dezember 2009, 17:44
von novoid
cofi hat geschrieben:Um mal `filter` einzuwerfen

Code: Alles auswählen

filter(lambda x: x >= 42, range(100))
OK. I see.

Sorry, ich bin leider zu unerfahren, um diesen Filter (den ich im Minimalbeispiel durchaus verstehe) auf das reale Beispiel mit den zwei Listen von dicts zu übertragen (siehe weiter oben mit "alarms" und "csvalarms").

Bin ich zu frech, wenn ich hier nach einer ein wenig ausformulierteren Lösung frage?

Verfasst: Donnerstag 17. Dezember 2009, 17:52
von cofi
Nein bist du nicht.

`filter` nimmt eine Funktion und eine Sequenz entgegen und testet jedes Element mit der Funktion. Ist das Ergebnis `True`, wird das Element in die neue Liste uebernommen.

Ungetestet (und unter der Bedingung, das ich das richtig verstanden habe):

Code: Alles auswählen

from functools import partial
def valid(alarm, row): 
    not (str(alarm['description']) == row['description'] and
           str(alarm['begin'])     == row['begin'] and
           str(alarm['end'])       == row['end'] and
           str(alarm['alarm'])     == row['alarm'])

for row in csvReader:
    valid = partial(valid, row=row)
    alarms = filter(valid, alarms)

Verfasst: Donnerstag 17. Dezember 2009, 18:07
von novoid
cofi hat geschrieben:

Code: Alles auswählen

from functools import partial
def valid(alarm, row): 
    str(alarm['description']) == row['description'] and \
    str(alarm['begin'])       == row['begin'] and \
    str(alarm['end'])         == row['end'] and \
    str(alarm['alarm'])       == row['alarm']

for row in csvReader:
    valid = partial(valid, row=row)
    alarms = filter(valid, alarms)
Hm, elegante Formulierung.

Aber ich verstehe es noch nicht ganz:

Pro Zeile in der csv wird aus alarms jedes Element herausgelöscht, wo die valid-Bedingungen true ergeben. Das ist also eine andere Art von alarms.remove(), die aber ohne for-Schleife auskommt, keinen Listeniterator manipuliert und daher im Gegensatz zu meiner Methode funktioniert. Habe ich das richtig verstanden?

Ich werd's mal auf jeden falls coden und ausprobieren...

Verfasst: Donnerstag 17. Dezember 2009, 18:27
von cofi
Hmm .. ich hab einen Fehler, momentan macht er genau das andere ;) Gleich mal fixen.

Um die Iteration kommt man nicht herum, hier ist sie allerdings nicht explizit. Im Prinzip wird eine neue Liste erzeugt und an die angehaengt, wenn das Praedikat (die Funktion) erfuellt ist.

Aber dein Problem kannst du auch ueber eine explizite for-Schleife loesen, wie auch schon erwaehnt wurde:

Code: Alles auswählen

for row in csvReader:
    for alarm in alarms[:]:
        ...

Verfasst: Donnerstag 17. Dezember 2009, 18:53
von novoid
cofi hat geschrieben:Hmm .. ich hab einen Fehler, momentan macht er genau das andere ;) Gleich mal fixen.
Bei mir kommt auch:
TypeError: valid() takes exactly 2 arguments (1 given)
in der filter-Zeile. Das ist Zeile 11 vom Posting von 17.52.
cofi hat geschrieben: Um die Iteration kommt man nicht herum, hier ist sie allerdings nicht explizit. Im Prinzip wird eine neue Liste erzeugt und an die angehaengt, wenn das Praedikat (die Funktion) erfuellt ist.

Aber dein Problem kannst du auch ueber eine explizite for-Schleife loesen, wie auch schon erwaehnt wurde:

Code: Alles auswählen

for row in csvReader:
    for alarm in alarms[:]:
        ...
Sorry, ich hab mit der doppelten Schleife immer ein Problem, weil ich die Schleifenliste mit dem Herauslöschen von Elementen modifiziere. Dabei kommt immer Blödsinn raus :-(

Ich glaube, dass man bei for element in liste niemals ein Element löschen darf.

Und somit stehe ich wieder am Anfang :-(

Verfasst: Donnerstag 17. Dezember 2009, 18:58
von cofi
novoid hat geschrieben:Bei mir kommt auch:
TypeError: valid() takes exactly 2 arguments (1 given)
in der filter-Zeile. Das ist Zeile 11 vom Posting von 17.52.
Du solltest allerdings nicht Zeile 10 weglassen, dort wird naemlich `valid` gecurried. Wobei es ja unguenstig ist, den selben Namen zu verwenden.
novoid hat geschrieben:Ich glaube, dass man bei for element in liste niemals ein Element löschen darf.
Ja, das ist schon richtig, aber du iterierst hier ueber eine Kopie der `alarms`-Liste und loescht die Elemente aus dem Original. Das geht.

Verfasst: Donnerstag 17. Dezember 2009, 20:58
von BlackJack
@novoid: Mir scheint Du übersiehst immer das *kopieren* der Liste. ``a = b`` ist etwas anderes als ``a = b[:]``. Letzteres kann man auch als ``a = list(b)`` schreiben. Vielleicht übersieht man es dann nicht mehr so schnell.

Wobei Dein Ausgangsproblem im Grunde nicht das übliche ist, und IMHO gar nicht existieren dürfte solange Du nur *ein* Element aus der Liste entfernst. Denn Du brichst die Schleife danach ja sofort mit ``break`` ab. Das sollte keine Probleme geben.

Das Original bekommt man mit `operator.itemgetter()` übrigens etwas kürzer (ungetestet):

Code: Alles auswählen

    get_fields = itemgetter('description', 'begin', 'end', 'alarm')
    for row in reader:
        for alarm in alarms:
            if map(str, get_fields(alarm)) == get_fields(row):
                alarm['description'] = DELETE_STRING
                alarms.remove(alarm)
                break