Dateien lesen und schreiben
Lesen und Schreiben mit `with open`, was r/w/a/x voneinander unterscheidet, der Zeilenumbruch am Ende jeder Zeile, warum `encoding` immer geschrieben wird, und pathlib für die kleinen Aufgaben.
Wie möchtest du lernen?
Das Problem, das wir lösen
Alles, was wir in zweiundzwanzig Kapiteln gebaut haben, verschwand in dem Moment, in dem das Programm endete.
basket = []
basket.append("pen")
print(basket)['pen']Starte es erneut, und der basket ist wieder leer. Listen, Dictionaries, Funktionen — all das lebt im Speicher, und der Speicher existiert genau so lange wie das Programm.
Das letzte Kapitel ging darum, Code zwischen Programmen zu teilen. Dieses geht um Daten — wie ein Lauf seine Arbeit für den nächsten Lauf oder für jemand anderen hinterlässt.
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("first line\n")
fh.write("second line\n")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(fh.read(), end="")first line
second lineDas Programm ist beendet, und notes.txt ist immer noch da.
Am Ende dieses Kapitels kannst du
- Dateien mit
with open(...)lesen und schreiben und sagen, warumwith - Sagen, was
"r","w","a"und"x"voneinander unterscheidet — und besonders, was"w"zerstört - Eine Datei Zeile für Zeile lesen und mit dem
\nam Ende jeder Zeile umgehen - Sagen, warum
encoding="utf-8"immer geschrieben wird pathlib.Pathfür die kleinen Aufgaben verwenden- Einen
FileNotFoundErrorund einenUnicodeDecodeErrorlesen
Voraussetzungen: Module und Imports.
Warum with
Eine Datei zu öffnen leiht sich etwas vom Betriebssystem, und eine Leihgabe muss zurückgegeben werden. Ohne Schließen erreicht das Geschriebene die Festplatte möglicherweise nie, und genug nicht geschlossene Dateien stoppen das Programm.
Ein with-Block erledigt diese Rückgabe selbst — die Datei wird geschlossen, wenn der Block endet, auch wenn etwas darin eine Ausnahme ausgelöst hat.
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(fh.closed)
print(fh.read())True
ValueError: I/O operation on closed file.(Vor der Fehlerzeile druckt Python auch einige Traceback-Zeilen; in diesem Kapitel wird nur die letzte Zeile gezeigt.)
Der Name fh existiert nach dem Block noch immer — der Gültigkeitsbereich funktioniert genau wie zuvor — aber die Datei ist zu. In diesem Kurs wird open() immer mit with geschrieben.
Lesen
Den gesamten Inhalt auf einmal bekommt man mit .read():
with open("notes.txt", "r", encoding="utf-8") as fh:
text = fh.read()
print(repr(text))
print(len(text))'first line\nsecond line\n'
23repr wird bewusst benutzt, denn ein einfaches print versteckt die Zeilenumbrüche. Eine Datei ist ein einziger langer Text, dessen Zeilenenden als \n darin sitzen — das zu verstehen ist die Hälfte dieses Kapitels.
Zeile für Zeile
.read() ist für eine große Datei eine schlechte Wahl, weil es alles in den Speicher lädt. Wenn man über die Datei selbst loopt, liest Python Zeile für Zeile:
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))'pen,15.0,3\n'
'bag,850.0,1\n'
'ink,120.0,2\n'Jede Zeile endet mit \n. Das ist die häufigste Verwirrung überhaupt — line == "pen" stimmt nicht, "pen\n" und "pen" sind verschiedene Dictionary-Schlüssel, und der Grund ist unsichtbar. (int() und float() ignorieren von selbst Leerzeichen und \n um die Zahl herum, sodass int("3\n") tatsächlich 3 ergibt — aber ein String bleibt genau so, wie er ist, das \n bleibt also in Vergleichen und im gedruckten Output.)
Der erste Schritt ist also fast immer .strip():
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
name, price, qty = line.strip().split(",")
print(f"{name:<5} {float(price) * int(qty):>8.2f}")pen 45.00
bag 850.00
ink 240.00.strip() dann .split(",") dann float() und int() — diese Folge wird beim Lesen von Dateien immer wieder vorkommen. Alles, was aus einer Datei kommt, ist Text, nie Zahlen; die Umwandlung aus Kapitel sieben wird hier auch gebraucht.
Wenn man alle Zeilen auf einmal braucht, gibt es zwei Wege:
with open("orders.txt", "r", encoding="utf-8") as fh:
lines = fh.read().splitlines()
print(lines)['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2'].splitlines() entfernt die Zeilenenden. Es gibt auch .readlines(), aber es behält das \n — daran stolpern die meisten.
Schreiben, und ein gefährlicher Buchstabe
with open("notes.txt", "w", encoding="utf-8") as fh:
fh.write("new\n")Die Datei enthielt zuvor old content. Jetzt:
new"w" leert die Datei — im Moment des Öffnens, bevor etwas geschrieben wird. Keine Warnung, kein Zurück.
Um zu behalten, was da ist, und ans Ende anzuhängen, "a":
with open("notes.txt", "a", encoding="utf-8") as fh:
fh.write("new\n")old content
newUnd um sich zu weigern, überhaupt zu schreiben, wenn die Datei bereits existiert, "x":
FileExistsError: [Errno 17] File exists: 'notes.txt'Die vier Modi in einem Satz: "r" liest, "w" löscht und schreibt, "a" hängt ans Ende, "x" legt nur an.
Mach eine Pause, bevor du"w"tippst. Echte Arbeit verliert man wegen diesem einen Buchstaben. Wenn die Datei, die du öffnest, nicht von deinem Programm erstellt wurde, überlege dir das Schreiben von"w"gut.
Das \n musst du selbst liefern
print bricht die Zeile für dich. write nicht.
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)'penbag'Der sauberste Weg, eine Liste von Zeilen zu schreiben, ist:
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
fh.write("\n".join(rows) + "\n")'pen\nbag\n'"\n".join(rows) setzt Umbrüche zwischen die Zeilen, und das abschließende + "\n" endet die Datei mit einem Zeilenumbruch — das ist die übliche Regel für Textdateien, und viele Tools erwarten das.
encoding="utf-8" — immer
Ohne encoding benutzt Python den Standard des Betriebssystems, der von Maschine zu Maschine variiert. Das Ergebnis ist ein Programm, das dort funktioniert, wo du bist, und dort bricht, wo jemand anderes ist.
Lesen mit der falschen ergibt:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byteSchreib also encoding="utf-8", jedes Mal, wenn du open() schreibst — beim Lesen wie beim Schreiben. Die einzige Ausnahme ist der Binärmodus ("rb", "wb"), wo die Frage nicht auftaucht, weil es keine Kodierung gibt.
pathlib — ein kurzer Pfad für kleine Aufgaben
Für eine kleine Datei kann der ganze with-Block schon viel wirken.
from pathlib import Path
path = Path("orders.txt")
print(path.exists())
print(len(path.read_text(encoding="utf-8").splitlines()))
out = Path("report.txt")
out.write_text("done\n", encoding="utf-8")
print(out.read_text(encoding="utf-8"), end="")True
3
doneread_text und write_text öffnen und schließen die Datei intern, also kein with nötig. Path kann mehr — .exists(), .name, .suffix, .parent — und Ordnerpfade werden mit / verbunden, wie in Path("data") / "orders.txt".
Was wann? Path, wenn die ganze Datei auf einmal gebraucht wird, with open(...), wenn Zeile für Zeile gelesen wird. Behalt eine Sache im Blick: write_text verhält sich immer wie "w" — wenn die Datei bereits existiert, wird ihr alter Inhalt ohne Warnung gelöscht. Es gibt keinen Weg anzuhängen; dafür brauchst du with open(..., "a").
Ein vollständiges Beispiel
orders.txt:
pen,15.0,3
bag,850.0,1
ink,120.0,2main.py:
"""Read an order file, total it, and write a report beside it."""
from pathlib import Path
TAX_RATE = 0.15
def read_order(path):
"""Each line is name,price,quantity. Blank lines are skipped."""
lines = []
for raw in path.read_text(encoding="utf-8").splitlines():
if not raw.strip():
continue
name, price, quantity = raw.split(",")
lines.append((name, float(price), int(quantity)))
return lines
def report(lines):
rows = []
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
rows.append(f"{name:<6} {quantity:>3} {amount:>9.2f}")
rows.append("-" * 20)
rows.append(f"{'total':<6} {'':>3} {total:>9.2f}")
return rows
def main():
order = read_order(Path("orders.txt"))
rows = report(order)
Path("report.txt").write_text("\n".join(rows) + "\n", encoding="utf-8")
print(Path("report.txt").read_text(encoding="utf-8"), end="")
print()
print("lines read:", len(order))
if __name__ == "__main__":
main()pen 3 51.75
bag 1 977.50
ink 2 276.00
--------------------
total 1305.25
lines read: 3Vier Dinge, die sich lohnt anzusehen.
Drei Funktionen mit drei getrennten Aufgaben. read_order liest von der Festplatte, report rechnet nur, und main schreibt. report erhält überhaupt keine Datei — es erhält eine Liste und gibt eine Liste zurück. Zum Testen braucht man also keine Datei, und in Kapitel achtundzwanzig wird genau das helfen.
Leere Zeilen werden vorab behandelt. Ein einzelnes \n am Ende der Datei erzeugt keine leere Zeile — .splitlines() entfernt es. Aber in einer von Hand bearbeiteten Datei ist eine echte leere Zeile (zwei \n hintereinander) in der Mitte oder am Ende völlig normal, und ohne Behandlung würde raw.split(",") nicht drei Dinge bekommen und einen ValueError auslösen. if not raw.strip(): continue erledigt es in einer Zeile.
float(price) und int(quantity) sind nicht optional, weil alles aus einer Datei als Text ankommt. Vergisst du sie, würde price * quantity stillschweigend den String wiederholen statt multiplizieren — die schlimmste Art von Bug, die Art, die nicht bricht.
Der Bericht geht in eine eigene Datei. orders.txt wird nie mit "w" geöffnet — die Eingabedatei bleibt unverändert. Das ist eine Gewohnheit, die sich lohnt: schreibe nicht in das, was du liest.
Wenn es schiefgeht
FileNotFoundError: [Errno 2] No such file or directory: 'orders.txt' Die Datei ist nicht da, oder du startest das Programm aus einem anderen Ordner. Der Pfad wird aus dem Ort bestimmt, von dem du es gestartet hast, nicht aus dem Ordner des Skripts. Drucke Path("orders.txt").resolve(), um zu sehen, wo es sucht.
UnicodeDecodeError: 'utf-8' codec can't decode byte ... Die Datei wurde in einer anderen Kodierung geschrieben, oder sie ist überhaupt keine Textdatei. Korrigiere das encoding, oder öffne sie mit "rb", wenn sie binär ist.
ValueError: I/O operation on closed file. Die Datei wurde außerhalb ihres with-Blocks verwendet. Lies, was du brauchst, innerhalb des Blocks.
ValueError: not enough values to unpack Irgendeine Zeile hat nicht die Kommas, die du erwartet hast — oft eine leere Zeile. Überspringe leere Zeilen, und im Zweifel print(repr(raw)).
Ich habe die Datei geschrieben, aber sie ist leer Sie wird gelesen, bevor der with-Block fertig ist. Schreibvorgänge erreichen die Festplatte, wenn die Datei geschlossen wird.
Alle meine Zeilen sind zu einer einzigen zusammengelaufen write bricht keine Zeilen. Das "\n" musst du selbst liefern.
Alles in meiner Datei ist weg Sie wurde mit "w" geöffnet. Benutze "r" zum Lesen und "a" zum Anhängen ans Ende.
Schritt 4 von 6 — Vorhersagen
Prüfe dein Verständnis
repr wird benutzt, sodass nichts versteckt wird. Was wird gedruckt?
# orders.txt
# pen,15.0,3
# bag,850.0,1
# ink,120.0,2
with open("orders.txt", "r", encoding="utf-8") as fh:
for line in fh:
print(repr(line))- A'pen,15.0,3' 'bag,850.0,1' 'ink,120.0,2'
- B'pen,15.0,3\nbag,850.0,1\nink,120.0,2\n'
- C'pen,15.0,3\n' 'bag,850.0,1\n' 'ink,120.0,2\n'
- D['pen,15.0,3', 'bag,850.0,1', 'ink,120.0,2']
Die Datei wird mit "w" geöffnet, aber nichts geschrieben. Was wird gedruckt?
# notes.txt already contains: important work
with open("notes.txt", "w", encoding="utf-8") as fh:
print("opened")
with open("notes.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- Aopened 'opened\n'
- BEin `FileExistsError`
- Copened 'important work\n'
- Dopened ''
Zwei Dinge werden getrennt geschrieben. Was steht in der Datei?
rows = ["pen", "bag"]
with open("out.txt", "w", encoding="utf-8") as fh:
for row in rows:
fh.write(row)
with open("out.txt", "r", encoding="utf-8") as fh:
print(repr(fh.read()))- A'pen\nbag\n'
- B'pen\nbag'
- C'pen bag'
- D'penbag'
Zum Antworten brauchst du ein Konto
Melde dich an, um deine Antworten zu prüfen
Die Fragen stehen oben, und sie im Kopf durchzuarbeiten ist der wichtige Teil. Melde dich an, um die Antworten, die Erklärungen und die dreistufigen Hinweise zu sehen.
Jetzt bist du dran
Erstelle eine Datei namens people.txt mit einem Namen und einer Zahl pro Zeile, getrennt durch ein Komma — mindestens vier Zeilen, und lass absichtlich eine leere Zeile in der Mitte.
Schreib dann summary.py mit drei Funktionen: eine liest die Datei in eine Liste von Paaren, eine macht aus dieser Liste Textzeilen (wer die größte Zahl hat, und die Summe), und main schreibt sie nach summary.txt. Vergiss nicht if __name__ == "__main__":.
Dann fünf Experimente:
- Entferne das
.strip()und drucke dann den letzten Teil jeder Zeile mitrepr(). Löstint()einen Fehler aus? (Nein —int("10\n")ergibt10.) Wo versteckt sich also das\n, und was passiert bei der leeren Zeile? - Lösche eine Zahl aus einer Zeile, sodass das Komma da ist, aber die Zahl nicht. Lies dir die Meldung durch.
- Ändere
"w"inmainzu"a"und starte das Programm dreimal. Was ist mitsummary.txtpassiert? - Versuche,
people.txtmit"w"zu öffnen... nicht. Mach eine Kopie und mach es mit der, und sieh dir an, was übrig bleibt. - Drucke
Path("people.txt").resolve()und starte das Programm dann aus einem anderen Ordner. Ändert sich der Pfad?
Das letzte erklärt einen Fehler, den fast jeder einmal macht: Das Programm funktionierte, dann wurde es von woanders gestartet und löste einen FileNotFoundError aus.
Schritt 6 von 6
Herausforderung — das Kapitel-Quiz
Zehn Fragen von leicht bis schwer. Die letzten sind absichtlich knifflig.
Melde dich an, um das Quiz zu machen