Liste komplett iterieren, während elemente entfernt werden

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
Benutzeravatar
Judge
User
Beiträge: 129
Registriert: Mittwoch 13. Juni 2012, 22:27
Wohnort: Ratingen
Kontaktdaten:

Hallo zusammen,

ich habe ein Problem mit der vollständigen Iterierung einer Liste:

Angenommen, ich habe folgende Liste "liste":

Code: Alles auswählen

liste = ['aaa', 'bbb', 'bbb', 'ccc']
Aus dieser Liste möchte ich nun einzelne Elemente herauslösen, nachdem diese verarbeitet wurden, das heißt: beispielsweise, nachem eine Regex-Prüfung darauf stattgefunden hat. Also dachte ich, gehe ich wie folgt vor:

Code: Alles auswählen

import re
pat = re.compile('^bbb$')
for elem in liste:
    if re.match(pat, elem):
        liste.remove(elem)
Hierbei behalte ich jedoch folgendes übrig:

Code: Alles auswählen

['aaa', 'bbb', 'ccc']
Ich glaube auch zu wissen warum das so ist (korrigiert mich, wenn ich falsch liege):
Python prüft das erste Element in der Liste ("aaa"); dieses matched nicht, also: Next.
Das zweite Element (index 1) matched; also wird es mit der Listenmethode ".remove" entfernt. Dabei rücken die nachfolgenden Elemente eine Position "nach links"; das 2. "bbb" an index 2 rückt auf index 1, das "ccc" auf index 3 rückt auf index 2. Python weiß: Er hat gerade index 1 verarbeitet, also ist als nächstes index 2 dran. Auf index 2 liegt aber ja nicht mehr das eigentlich "nächste" Element, sondern "ccc". Hierdurch wird das 2. "bbb" nie bearbeitet.

Frage 1: Alles richtig soweit, oder bin ich da auf dem Holzweg?

Frage 2: Wie mache ich dieses denn "Korrekt", also: So das alle Elemente einmal verarbeitet werden? Eine Methode wäre ja alles was NICHT matched in eine neue Liste zu kopieren und die alte anschließend zu verwerfen; das fühlt sich aber irgendwie unsauber an und ich vermute, das das unerwünschte Seiteneffekte haben könnte. Das wird doch wohl auch irgendwie in der Liste gehen, oder?

Danke schonmal für Eure Hilfe!

LG
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Deine Vermutung bezueglich der Ursache ist korrekt. Der Iterator einer Liste merkt sich im Grunde intern einfach nur einen Index, und dadurch kommt es durch eine solche gleichzeitige Veraenderung zu deinem Problem.

Wo du falsch liegst ist bei der Beurteilung ob es unsauber waere, neue Listen zu erzeugen. Python ist dynamisch, und eh relativ langsam. Es ist daher immer besser, Daten als "read-only" zu betrachten, wenn das geht. Zur Not kann man auch bestehende Datenstrukturen modifizieren (das ist beileibe nicht verboten), aber man sollte es wenn moeglich explizit machen. Fuer deinen Fall zB so:

Code: Alles auswählen

def partition(predicate, iterable):
      matched, unmatched = [], []
      for item in iterable:
            if predicate(item): 
                matched.append(item)
            else:
                unmatched.append(item)
       return matched, unmatched
       
 data = [1, 2, 3, 4]
 
to_work_on, data[:] = partition(lambda x: x % 2)

partition ist in diesem Fall einen generische Funtktion, die ich trotz nochmaliger Suche gerade nicht in der Standardbibliothek gefunden habe. predicate muss einfach ein callable sein, das kannst du als zB mit deinem re.match bauen, oder einfach eine kleine, lokale Funktion basteln.
Benutzeravatar
snafu
User
Beiträge: 6908
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Im Grunde ist es nicht mal so wichtig, wodurch dieser Effekt entsteht. Man sollte sich einfach merken, dass alle Arten von Iterationsvorgängen ein undefiniertes Verhalten haben, sobald man das Objekt, über das iteriert wird, während der Iteration verändert. Das Erstellen einer neuen Liste ist hierbei nicht unsauber, sondern der übliche Weg in Python. Bei wirklich großen Datenmengen kann `yield` eine Alternative sein, um Daten on-the-fly zu verarbeiten.

Die Python-Syntax macht es einem übrigens leicht, Filter-Operationen zu implementieren:

Code: Alles auswählen

nums = range(10)
odds = [n for n in nums if n % 2 != 0]
print(odds)
Benutzeravatar
Kebap
User
Beiträge: 786
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

Judge hat geschrieben: Frage 2: Wie mache ich dieses denn "Korrekt", also: So das alle Elemente einmal verarbeitet werden? Eine Methode wäre ja alles was NICHT matched in eine neue Liste zu kopieren und die alte anschließend zu verwerfen; das fühlt sich aber irgendwie unsauber an und ich vermute, das das unerwünschte Seiteneffekte haben könnte. Das wird doch wohl auch irgendwie in der Liste gehen, oder?
Dein Gefühl täuscht dich hier.

Man erstellt in den allermeisten Fällen einfach eine neue Liste mit gewünschtem Inhalt und lässt die alte alt sein.
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Benutzeravatar
Judge
User
Beiträge: 129
Registriert: Mittwoch 13. Juni 2012, 22:27
Wohnort: Ratingen
Kontaktdaten:

__deets__ hat geschrieben:Deine Vermutung bezueglich der Ursache ist korrekt. Der Iterator einer Liste merkt sich im Grunde intern einfach nur einen Index, und dadurch kommt es durch eine solche gleichzeitige Veraenderung zu deinem Problem.

Wo du falsch liegst ist bei der Beurteilung ob es unsauber waere, neue Listen zu erzeugen. Python ist dynamisch, und eh relativ langsam. Es ist daher immer besser, Daten als "read-only" zu betrachten, wenn das geht. Zur Not kann man auch bestehende Datenstrukturen modifizieren (das ist beileibe nicht verboten), aber man sollte es wenn moeglich explizit machen. Fuer deinen Fall zB so:

Code: Alles auswählen

def partition(predicate, iterable):
      matched, unmatched = [], []
      for item in iterable:
            if predicate(item): 
                matched.append(item)
            else:
                unmatched.append(item)
       return matched, unmatched
       
 data = [1, 2, 3, 4]
 
to_work_on, data[:] = partition(lambda x: x % 2)

partition ist in diesem Fall einen generische Funtktion, die ich trotz nochmaliger Suche gerade nicht in der Standardbibliothek gefunden habe. predicate muss einfach ein callable sein, das kannst du als zB mit deinem re.match bauen, oder einfach eine kleine, lokale Funktion basteln.
Danke schonmal für die Erklärung, von der ich aber bisher leider erst die Hälfte verstehe ~;) Ich dachte beim Kopieren kann man sich ggf. Ärger einhandeln, weil man z.B. relativ viel Speicher (x2) verbraucht und das kein guter Stil ist oder so ... bei diesen Listen: EGAL, aber man sollte ja immer groß denken ;) .

Die letzte Zeile Deines Beispiels verstehe ich nicht. ich kenne bisher weder die Schreibweise "to_work_on, data[:]", noch habe ich bisher mit lambda gearbeitet oder verstehe was das tut. Python ist halt auch meine erste Programmiersprache, weshalb ich das Wissen nicht aus anderen Erfahrungen ziehen kann.
snafu hat geschrieben:Im Grunde ist es nicht mal so wichtig, wodurch dieser Effekt entsteht. Man sollte sich einfach merken, dass alle Arten von Iterationsvorgängen ein undefiniertes Verhalten haben, sobald man das Objekt, über das iteriert wird, während der Iteration verändert. Das Erstellen einer neuen Liste ist hierbei nicht unsauber, sondern der übliche Weg in Python.
Wird gemerkt, Danke :)
snafu hat geschrieben:Bei wirklich großen Datenmengen kann `yield` eine Alternative sein, um Daten on-the-fly zu verarbeiten.

Die Python-Syntax macht es einem übrigens leicht, Filter-Operationen zu implementieren:

Code: Alles auswählen

nums = range(10)
odds = [n for n in nums if n % 2 != 0]
print(odds)
`yield` habe ich auch leider noch nicht verstanden ... schaue ich mir morgen mal genauer an, da mir das nun schon öfter begegnet ist; und die Erklärungen von freundlichen Helfern sollte ich schon langsam mal verstehen können ;)
Mir ist nicht ganz klar was Du mit Deinem Code-Beispiel sagen möchtest.
Benutzeravatar
kbr
User
Beiträge: 1510
Registriert: Mittwoch 15. Oktober 2008, 09:27

@Judge: __deets__ Beispiel läuft so nicht und 'data[:]' in der letzten Zeile ist auch sinnfrei.
Was snafu Dir zeigen wollte ist, dass Du Deinen Pattern-Filter kompakter schreiben kannst:

Code: Alles auswählen

import re
pat = re.compile('^bbb$')
liste = ['aaa', 'bbb', 'bbb', 'ccc'] 
liste = [item for item in liste if not pat.match(item)]
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

In der Dokumentation zum itertools Modul gibt es übrigens eine Implementation von partition die mit zwei Zeilen auskommt und mit Iteratoren statt Listen arbeitet.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

@kbr das Beispiel war aus dem Kopf getippt, nach Korrektur der Einrueckung & des fehlenden Arguments sieht's so aus, und laeuft:

Code: Alles auswählen


def partition(predicate, iterable):
    matched, unmatched = [], []
    for item in iterable:
        if predicate(item):
            matched.append(item)
        else:
            unmatched.append(item)
    return matched, unmatched

data = [1, 2, 3, 4]

to_work_on, data[:] = partition(lambda x: x % 2, data)
Und das data[:] ist keineswegs sinnfrei im Kontext der Frage danach, wie man eine bestehende Liste veraendert. Ja, konstruiert in diesem reduzierten Beispiel, aber das dient der Illustration - es tut ja auch jenseits dieser Anweisung nichts sinnvolles.
Benutzeravatar
Judge
User
Beiträge: 129
Registriert: Mittwoch 13. Juni 2012, 22:27
Wohnort: Ratingen
Kontaktdaten:

Danke an alle für die netten Erklärungen :)
Antworten