Zum Hauptinhalt springen

VERY Best Practice: Arbeiten in Python mit Pfaden - Teil 2

Immer noch das Problem: Ordner oder Laufwerke katalogisieren

Im letzten Eintrag haben wir in einer Lösung von weniger als zehn Zeilen mit einer rekursiven Funktion die Möglichkeit geschaffen, Ordner zu scannen und die Dateien nach Änderungsdatum und Dateigröße auswertbar zu machen.

Aufbauend auf diesem Beispiel möchte ich die Latte nochmal etwas höher legen und noch bessere Alternativen aufzeigen.

Pfade verketten mit Pathlib

Alter Wein in neuen Schläuchen?

Die finale Lösung für Pfadverkettung sah im früheren Beispiel so aus:

path_file = os.sep.join([path_dir, filename])

Das Positive daran ist, dass die Lösung unabhängig vom Betriebssystem funktioniert und Strings nicht direkt mit „+“-Zeichen oder String-Formatierungen kombinieren muss.

Es besteht jedoch noch ein Fehlerpotential, nämlich wenn jemand den Verzeichnispfad versehentlich oder aus falscher Überzeugung mit einem abschließenden Pfadtrenner definiert.

path_dir: str = r"C:/Users/sselt/Documents/blog_demo/"  # abschließender Trenner

filename: str = "some_file"

path_file = os.sep.join([path_dir, filename])

# C:/Users/sselt/Documents/blog_demo/\some_file

Dieses Beispiel zeigt zwar funktionierenden Code, der Aufruf des Pfades wird aufgrund des letzten fehlerhaften Trenners jedoch einen Fehler verursachen. Solche Fehler können ständig auftauchen, wenn User die Pfade in Config-Files, weit weg vom Code, pflegen und nicht auf die Konventionen achten.

Seit Python 3.4 gibt es jedoch eine bessere Lösung in Form des pathlib-Moduls. Es deckt die datei- und ordnerbezogenen Funktionen des os-Moduls von Python über einen objektorientierten Ansatz ab.

Hier zunächst die alte Variante:

import os

path = "C:/Users/sselt/Documents/blog_demo/"

os.path.isdir(path)

os.path.isfile(path)

os.path.getsize(path)

 

Und hier die neue Alternative:

from pathlib import Path

path: Path = Path("C:/Users/sselt/Documents/blog_demo/")

path.is_dir()

path.is_file()

path.stat().st_size

Beides liefert hier genau dieselben Ergebnisse. Warum ist die zweite Variante so viel besser?

Objektorientiert und fehlertoleranter

Zunächst einmal sind die Aufrufe objektorientiert, was Geschmackssache sein kann, aber mir persönlich sehr viel besser gefällt. Es gibt hier ein Objekt wie die Pfaddefinition, und die hat Eigenschaften und Methoden.

Spannender ist aber ein hier angewendetes Beispiel für das Überladen von Operatoren:

filename: Path = Path("some_file.txt")

path: Path = Path("C:/Users/sselt/Documents/blog_demo")

print( path / filename )

# C:\Users\sselt\Documents\blog_demo\some_file.txt

Die Division von zwei Pfaden sieht hier zunächst wie ungültiger Code aus. Tatsächlich wurde lediglich im Path-Objekt der Divisionsoperator so überladen, dass er wie eine Pfadverkettung funktioniert.

Neben diesem Syntactic Sugar werden über Path-Objekte noch andere typische Fehler abgefangen:

filename: Path = Path("some_file.txt")

# hier path mit überflüssigem Trenner am Schluss

path: Path = Path("C:/Users/sselt/Documents/blog_demo/")

# hier path mit doppeltem Trenner

path: Path = Path("C:/Users/sselt/Documents/blog_demo//")

# hier path völlig durcheinander

path: Path = Path("C:\\Users/sselt\\Documents/blog_demo")  # hier ein wilder Mix

# alle Varianten führen zum selben Ergebnis

print(path/filename)

# C:\Users\sselt\Documents\blog_demo\some_file.txt


Diese Variante ist also nicht nur schöner, sondern auch robuster gegenüber Falscheingaben. Neben anderen Vorteilen ist der Code auch völlig unabhängig vom Betriebssystem. Man definiert zwar nur ein generisches Path-Objekt, auf einem Windows-System manifestiert sich dieses aber als „WindowsPath“ und auf einem Linux-System als „PosixPath“.

Die meisten Funktionen, die sonst einen String als Pfad erwarten, kommen auch direkt mit einem „Path“ klar. In den seltenen Ausnahmen kann man einfach mit „str(Path)“ das Objekt wieder auflösen.

Ablaufen der Pfade mit os.walk

In der Lösung des letzten Blogs verwendete ich os.listdir, os.path.isdir und eine rekursive Funktion, um durch den Pfadbaum zu iterieren und zwischen Ordnern und Dateien zu unterscheiden.

Eine schönere Lösung bietet os.walk. Die Methode erzeugt keine Liste, sondern erstmal einen Iterator, den man Zeile für Zeile abrufen kann. Die Ergebnisse beinhalten dann jeweils den Ordnerpfad und in einer Liste alle Dateinamen unter diesem Pfad. Das Ganze passiert von sich aus rekursiv, so dass man mit einem Aufruf alle Daten erhält.

Die bessere Lösung mit os.walk und Pathlib

Wenn man beide eben vorgestellten Techniken kombiniert, erhält man eine neue Lösung, die schlanker ist, völlig betriebssystemunabhängig, robuster gegenüber inkonsequenten Pfadformaten und frei von explizten Rekursionen:

filesurvey = []

for row in os.walk(path):   # row beinhaltet jeweils einen Ordnerinhalt

    for filename in row[2]:  # row[2] ist ein tupel aus Dateinamen

        full_path: Path = Path(row[0]) / Path(filename)   # row[0] ist der Ordnerpfad

        filesurvey.append([path, filename, full_path.stat().st_mtime, full_path.stat().st_size])

   

Wenn man hier in Bezug auf Best Practice noch eins nachlegen kann, dann schreibt mir! Ich freu mich auf Feedback.

Lesen Sie hier den ersten Teil des Blogbeitrags.

Stefan Seltmann
Dein Ansprechpartner
Stefan Seltmann
Lead Expert
Stefan liebt das Programmieren, vor allem rund um Data Engineering und Data Science, und arbeitet quasi in seinem Hobby. Gerade für Softwareentwicklung mit Python und/oder Spark punktet er als b.telligents Telefonjoker.
#CodeFirst, #TestMore, #CodeDoctor