komisches Phaenomen bei os.walk und Listenmanipulation

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
shakebox
User
Beiträge: 175
Registriert: Montag 31. März 2008, 17:01

Montag 19. Mai 2008, 10:30

Hallo!

Als Ergebnis zu dem Thread von letzter Woche bin ich gerade dabei, mit dem os.walk rumzuprobieren. Dabei hab ich ein seltsames Phaenomen, das ich mir nicht erklaeren kann. Wenn ich folgenden Code habe:

Code: Alles auswählen

import os
for root, dirs, files in os.walk('/project/010_30/scans/012_30_1/scans/012_30_1/2048x1168'):
    for i in dirs:
        if i.startswith('proxies_'):
            print i
bekomme ich folgendes Ergebnis zurueckgeliefert:
proxies_quarter
proxies_half
proxies_full
wenn ich aber das gefundene Ergebnis so aus der dirs-Liste entferne, damit der walk-Befehl nicht in das gefundene Verzeichnis reingeht, tritt was komisches auf:

Code: Alles auswählen

import os
for root, dirs, files in os.walk('/project/010_30/scans/012_30_1/scans/012_30_1/2048x1168'):
    for i in dirs:
        if i.startswith('proxies_'):
            print i
            dirs.remove(i)
proxies_quarter
proxies_half
Eigentlich will ich schon weiterhin alle drei Fundstellen als Ergebnis haben, nur soll eben der os.walk nicht da reinverzweigen, da die Ordner spaeter mal komplett geloescht werden sollen und so der Inhalt nicht mehr interessiert.

Kann mir jemand erklaeren warum das 'proxies_half' nicht ausgespuckt wird und demnach ja auch nicht weiterverarbeitet werden kann? Hab ich da irgendwo nen groben Denkfehler drin? Wenn ja, wie kann man das besser machen?

Danke und Gruss, Shakebox

PS: @lunar: ja, das ist jetzt nicht mit der Methode gemacht die Du mir vorgeschlagen hast. Ich glaub ich muss das erstmal mit if usw. machen, damit ich halbwegs verstehe was ich da tue. Wenn das klappt will ich mir Deinen Vorschlag nochmal anschauen. Was mir eigentlich (wenn obiges Phaenomen nicht waere) an der os.walk-Methode besser gefaellt ist dass ich mit dem remove() dafuer sorgen kann dass in Unterordner gar nicht reingegangen wird. Bei path.walk wird ja erst die komplette Liste erstellt und dann erst auf match geprueft, oder?
shakebox
User
Beiträge: 175
Registriert: Montag 31. März 2008, 17:01

Montag 19. Mai 2008, 10:44

Ergaenzung:

jetzt hab ich mal noch print-Ausgaben zum Testen eingebaut:

Code: Alles auswählen

import os
for root, dirs, files in os.walk('/project/010_30/scans/012_30_1/scans/012_30_1/2048x1168'):
    print dirs
    for i in dirs:
        if i.startswith('proxies_'):
            print i
    print dirs
und bekomme da dann folgendes Ergebnis:
['proxies_quarter', 'original', 'proxies_half', 'proxies_full']
proxies_quarter
proxies_half
['original', 'proxies_full']
['128x96']
['128x96']
[]
[]
[]
[]
Warum wird das 'proxies_full' nicht mit als gefundener Wert ausgespuckt und dann aus der Liste entfernt? Eigentlich muesste die Liste doch nach dem Durchlauf nur noch ['original'] heissen und nicht mehr ['original', 'proxies_full'].

Danke!
Benutzeravatar
jens
Moderator
Beiträge: 8483
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Montag 19. Mai 2008, 11:16

Ich würde es ehr so machen:

Code: Alles auswählen

filtered_dirs = [i for i in dirs if not i.startswith("proxies_")]
IMHO eh eine schlechte idee, elemente aus einer liste zu löschen, wenn man über diese iteriert ;)

CMS in Python: http://www.pylucid.org
GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
shakebox
User
Beiträge: 175
Registriert: Montag 31. März 2008, 17:01

Montag 19. Mai 2008, 11:25

jens hat geschrieben:IMHO eh eine schlechte idee, elemente aus einer liste zu löschen, wenn man über diese iteriert ;)
Merci, aber wenn ich den Kollegen BlackJack in dem Ursprungsthread nicht ganz falsch verstanden habe, hat er mir genau das vorgeschlagen :lol:

Obiges Beispiel ist natuerlich nur ein kleines Stueck aus dem eigentlichen Script. Ich hab das nur so konstruiert, damit man das komische Verhalten sehen kann.

So wie ich os.walk verstanden habe faengt das bei topdown-Methode auf oberster Ebene an. Das wird dann mein root-Element fuer den ersten Schleifendurchlauf. Die im Ausgangsordner enthaltenen Ordner und Files landen als Listen in dirs und files. im naechsten Durchgang werden dann alle Eintraege aus 'dirs' zu neuen 'roots' und so geht das weiter. Wenn ich jetzt Fundstuecke aus 'dirs' im ersten Durchgang loesche, werden diese nicht zu 'roots' und der ganze os.walk-Durchgang muesste doch schneller gehen.

So hatte ich Blackjack verstanden. Wenn das falsch war bitte ich um Erleuchtung, wie das eigentlich gemeint war :D

Trotzdem wundert mich das Verhalten oben. Ist das irgendwie erklaerbar oder darf man das eben bei so ner Iteration einfach nicht machen?
BlackJack

Montag 19. Mai 2008, 11:39

Ich habe zwar vorgeschlagen die Liste zu verändern, aber nicht *so*, weil das ja, wie man sieht, nicht funktioniert. :-)

Das Verhalten ist im Grunde recht einfach erklärbar: Die Iteration benutzt einen internen Zähler als Index, der in jedem Durchlauf um 1 erhöht wird. Wenn Du ein Element aus der Liste entfernst, rücken alle folgenden Elemente um eine Position in der Liste nach vorne. Davon bekommt aber die Schleifenlogik nichts mit und erhöht brav den Index und "überspringt" deshalb das Element nach dem gerade entfernten Element.

Das `remove()` ist übrigens auch sonst keine so gute Idee, weil das die Liste nach dem Argument linear von vorne durchsucht.

Ich würde eine neue Liste mit den Namen aufbauen, die bleiben sollen und deren Inhalt dann der alten Liste zuweisen. Das geht mit der "slice"-Notation: ``alte_liste[:] = neue_liste``.
shakebox
User
Beiträge: 175
Registriert: Montag 31. März 2008, 17:01

Montag 19. Mai 2008, 11:44

hm, ok :)

auf das remove kam ich durch die offizielle Doku des os.walk-Befehls. Da steht am Schluss naemlich folgendes Beispiel:

Code: Alles auswählen

from os.path import join, getsize
    for root, dirs, files in walk('python/Lib/email'):
        print root, "consumes",
        print sum([getsize(join(root, name)) for name in files]),
        print "bytes in", len(files), "non-directory files"
        if 'CVS' in dirs:
            dirs.remove('CVS')  # don't visit CVS directories
ist da jetzt substantiell ein Unterschied oder ist auch dieses Beispiel dann nicht 'optimal', sprich kann unter bestimmten Umstaenden dann Unsinn machen?

Danke!
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Montag 19. Mai 2008, 12:27

Da wird beim Entfernen nicht gleichzeitig über `dirs` iteriert, sollten also keine unerwarteten Nebeneffekte auftreten.
shakebox
User
Beiträge: 175
Registriert: Montag 31. März 2008, 17:01

Montag 19. Mai 2008, 12:35

ah, zum Problem wird das also nicht wegen der aeusseren for-Schleife mit den root, dirs, files sondern wegen der inneren mit 'for i in dirs'!? Jetzt wird das nachvollziehbarer, danke.
Benutzeravatar
jens
Moderator
Beiträge: 8483
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Montag 19. Mai 2008, 12:38

Eigentlich ein interessantes Thema.
Mag jemand über das Löschen von von Elemente in einer for-Schleife auf der Seite [wiki]Tutorial/Listen[/wiki] schreiben?

CMS in Python: http://www.pylucid.org
GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
lunar

Montag 19. Mai 2008, 12:47

Du musst das Löschen aus der Liste vom Iterieren über die Liste trennen:

Code: Alles auswählen

for root, dirs, files in os.walk('foo'):
    # ausgeben
    for dir in dirs:
        if dir.startswith('proxies_'):
             print dir
    # filtern
    dirs[:] = [d for d in dirs if not d.startswith('proxies_')]
shakebox
User
Beiträge: 175
Registriert: Montag 31. März 2008, 17:01

Montag 19. Mai 2008, 12:53

also wenn ich da tatsaechlich ne Kopie mache von der dirs-Liste und ueber die eine Kopie iteriere und aus der anderen die Elemente entferne scheint das sauber zu funktionieren. Soweit gut.

Nur werde ich hieraus leider noch nicht ganz schlau:
Das `remove()` ist übrigens auch sonst keine so gute Idee, weil das die Liste nach dem Argument linear von vorne durchsucht.

Ich würde eine neue Liste mit den Namen aufbauen, die bleiben sollen und deren Inhalt dann der alten Liste zuweisen. Das geht mit der "slice"-Notation: ``alte_liste[:] = neue_liste``.
Wie kann ich denn ne Liste aufbauen mit den Namen die bleiben sollen, ohne zu sagen "alles ausser das gefundene"? Denn genau das macht ja das remove doch eigentlich, oder? Ich versteh das mit dem linear durchsuchen schon. Aber wenn ich nach jedem einzelnen Durchlauf die Liste irgendwie komplett neu machen lasse mit den uebrigen Elementen, ist das doch auch nicht effektiv!? Da steh ich jetzt grad irgendwie noch auf dem Schlauch.

Danke!
shakebox
User
Beiträge: 175
Registriert: Montag 31. März 2008, 17:01

Montag 19. Mai 2008, 12:57

@lunar

ist das effektiv? Dann werden die ganzen Match-Checks (und es werden spaeter mal mehr als nur das eine "startswith") ja zweimal gemacht. Eigentlich geht man so die dirs-Liste doch komplett zweimal durch, es ist ja eigentlich zweimal die gleiche for-Schleife, oder nicht?
lunar

Montag 19. Mai 2008, 13:05

Effektiver zumindest als der Einsatz von ``remove``, da man hier mit zwei Iterationen auskommt, während ``remove`` zu wiederholten Iterationen führt (je nach Anzahl und Position der zu entfernenden Elemente).

Man kann die ganze Sache auch mit zwei Listen lösen und sich so eine Iteration sparen: http://paste.pocoo.org/show/51367/

Wir kommen ``timeit`` immer näher ;)
shakebox
User
Beiträge: 175
Registriert: Montag 31. März 2008, 17:01

Montag 19. Mai 2008, 13:15

ok, das mit dem append sieht recht elegant und selbst fuer mich verstaendlich aus :) Das nehm ich jetzt erstmal, das laeuft bei Tests jetzt auch brauchbar flott.

Danke allerseits, wenn ich ne Runde weiter bin mit meinem Verstaendnis von Python kann ich mir das ja dann nochmal anschauen und es noch besser machen!
Antworten