Iterierte Liste manipulieren

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
novoid
User
Beiträge: 6
Registriert: Donnerstag 17. Dezember 2009, 16:08

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.
novoid
Python Hobbyist
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hallo!

Am einfachsten ist es, wenn du eine neue Liste erzeugst, welche alle gewünschten Elemente enhält.
Das Leben ist wie ein Tennisball.
novoid
User
Beiträge: 6
Registriert: Donnerstag 17. Dezember 2009, 16:08

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 ...
novoid
Python Hobbyist
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

Code: Alles auswählen

for element in liste[:]:
   if element < 42:
      liste.remove(element) 
Einfach eine Kopie der Liste erzeugen
the more they change the more they stay the same
novoid
User
Beiträge: 6
Registriert: Donnerstag 17. Dezember 2009, 16:08

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.
novoid
Python Hobbyist
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

@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))
novoid
User
Beiträge: 6
Registriert: Donnerstag 17. Dezember 2009, 16:08

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?
novoid
Python Hobbyist
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

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)
Zuletzt geändert von cofi am Donnerstag 17. Dezember 2009, 18:29, insgesamt 2-mal geändert.
novoid
User
Beiträge: 6
Registriert: Donnerstag 17. Dezember 2009, 16:08

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...
novoid
Python Hobbyist
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

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[:]:
        ...
novoid
User
Beiträge: 6
Registriert: Donnerstag 17. Dezember 2009, 16:08

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 :-(
novoid
Python Hobbyist
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

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.
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
Antworten