7. Ein- und Ausgabe#

Ein Programm, das eine sinnvolle Aufgabe bearbeitet, wird immer in irgendeiner Weise kommunizieren, also Informationen entgegennehmen und insbesondere die Resultate ausgeben. Für beiden Prozesse sind verschiedene Szenarien denkbar.

Im Kapitel 2 hatten wir bereits die input()-Funktion kennengelernt, die es erlaubt, aus dem Programm heraus den Benutzer um eine Eingabe zu bitten. Eine Alternative wäre die Angabe von Parametern direkt beim Aufruf des Programms von der Kommandozeile. Auch die Verwendung eines graphischen Benutzerinterfaces ist denkbar.

Bei komplexeren Problemstellungen ist es aber häufig nicht sinnvoll, alle Parameter per Hand einzugeben, und vor allem diesen Prozess beim Programmstart jedes Mal durchlaufen zu müssen. Dann ist es sinnvoller, vom Programm benötigte Informationen in einer Datei zu hinterlegen und dann vom Programm einlesen zu lassen. In manchen Fällen kann es auch sinnvoll oder erforderlich sein, Daten aus dem Internet in das Programm zu laden.

Auch die Ausgabe von Ergebnissen kann auf verschiedene Weise erfolgen. In unseren Beispielen haben wir bisher die Möglichkeit genutzt, Daten auf dem Bildschirm auszugeben. Benutzt man ein graphisches Benutzerinterface, könnte man auch dort Informationen ausgeben. Größere Datenmengen wird man dagegen in den meisten Fällen in einer Datei abspeichern. Natürlich ist es auch möglich, die Ausgabe ins Internet zu senden, eine Aufgabe, die jeder Webserver erledigt.

Gerade im natur- und ingenieurwissenschaftlichen Bereich, aber auch in anderen Bereichen, die mit großen Datenmengen umgehen müssen, wird man die Daten oft grafisch geeignet darstellen. Hier stellt sich häufig die Frage, ob man für die Grafik benötigte Daten zunächst abspeichert oder aber direkt weiterverarbeitet. Ein wesentlicher Faktor wird dabei der Aufwand für die Erzeugung der Daten sein. Es ist sehr ärgerlich, die Ergebnisse einer mehrtägigen Rechnung nur deswegen zu verlieren, weil man in der Auswertung oder der Erzeugung der Grafik einen Fehler eingebaut hat. Können die Daten dagegen sehr schnell erzeugt werden, kann der Weg über eine Zwischenspeicherung der Daten lästig sein, beispielsweise wenn man schnell die Auswirkung von Parameteränderungen im Ergebnis ansehen möchte.

Wie schon aus diesen einleitenden Bemerkungen deutlich wird, kann die Ein- und Ausgabe von Daten ein im Detail komplexes Thema sein. Wir wollen im Folgenden zunächst relativ knapp auf die Eingabe über die Kommandozeile und die Tastatur eingehen und auch noch einmal einen kurzen Blick auf die print()-Funktion werfen. Danach werden wir uns vor allem mit dem Lesen und Schreiben von Dateien beschäftigen. Für speziellere Aspekte werden wir in weiterführenden Hinweisen zeigen, dass Python für viele der potentiell anfallenden Aufgaben nützliche Module in der Standardbibliothek bereit hält.

7.1. Eingabe über die Kommandozeile und die Tastatur#

Wenn man Programme von der Kommandozeile aufruft, ist es nicht unüblich, dabei Parameter zu übergeben. Programmiersprachen wie Fortran, C und Python bieten die Möglichkeit, auf diese Parameter innerhalb des Programms zuzugreifen. Die Alternative, solche Parameter im Programmcode selbst zu definieren, hat den Nachteil, dass man zur Änderung eines Parameters den Code verändern muss, was im Allgemeinen keine gute Idee ist, unter anderem weil man dabei vielleicht andere unbeabsichtigte Änderungen am Programm vornehmen könnte.

Wir demonstrieren das Vorgehen mit einem kleinen Beispielprogramme namens beispiel_1.py, das einen vom Benutzer vorgegebenen Text mehrfach ausgeben kann. Das erste Argument beim Aufruf soll der Text sein, das zweite Argument gibt dann die Zahl der Wiederholungen an.

1# beispiel_1.py
2import sys
3
4print(sys.argv)
5str = sys.argv[1]
6nmax = int(sys.argv[2])
7for n in range(nmax):
8    print(str)

Wir verwenden hier das argv-Attribute aus dem sys-Modul der Python-Standardbibliothek. Hierbei steht argv für argument vector. In Zeile 4 geben wir dieses Attribut zunächst aus, um zu sehen, was es alles enthält. Anschließend bestimmen wir aus zwei Einträgen in diesem Attribut in den Zeilen 5 und 6 den auszugebenden Text und die Zahl der Wiederholungen.

Führen wir das Programm beispiel_1.py mit den Parametern hallo und 3 aus, so erhalten wir die folgende Ausgabe.

1$ python beispiel_1.py Hallo 3
2['beispiel_1.py', 'Hallo', '3']
3Hallo
4Hallo
5Hallo

Zeile 1 gibt nach dem $-Prompt nochmals die Eingabe an, und dann folgt in den Zeilen 2 bis 5 die erzeugte Ausgabe. In Zeile 2 sehen wir, dass sys.argv eine Liste enthält, deren erster Eintrag dem Namen des Python-Skripts entspricht, dem sich in den folgenden Einträgen die angegebenen Parameter anschließen. Wie wir sehen, sind die alle Einträge Zeichenketten. Damit erklärt sich, warum wir in Zeile 6 unseres Beispielskripts eine Umwandlung in einen Integer vornehmen mussten. Die obigen Zeilen 3 bis 5 enthalten dann die erwartete Ausgabe, nämlich dreimal den Text Hallo.

Weiterführender Hinweis

Es kann vorkommen, dass die Zahl der Parameter deutlich größer ist als in unserem kleinen Beispiel und dass für eine Reihe von Parametern Defaultwerte definiert sind. Man wird dann meistens nur eine reduzierte Zahl von Parametern angeben, die entsprechend gekennzeichnet werden müssen, um eine eindeutige Zuordnung zu erlauben. Diese Situation ähnelt dem was wir im Kapitel 5.7 für Funktionsaufrufe mit Schlüsselworten und Defaultwerten kennengelernt hatten. In solchen Fällen ist es ratsam, sich das argparse-Modul der Python-Standardbibliothek anzusehen. Da dieses Modul relativ komplexe Möglichkeiten bietet, steht auch ein spezielles Tutorial zur Verfügung.

Alternativ zur Angabe von Parametern beim Programmaufruf kann man die Parameter auch durch das Programm abrufen lassen. Hierzu dient die input()-Funktion, die wir bereits in Kapitel 2 kennengelernt haben. Auch hier ist wieder zu beachten, dass die Eingaben im Programm als Zeichenketten vorliegen und je nach Bedarf umgewandelt werden müssen.

Warnhinweis

Man kann die über die input()-Funktion erhaltene Eingabe mit Hilfe der eval()-Funktion auch als Python-Ausdruck auswerten lassen. Was auf den ersten Blick vielleicht vor allem interessante Möglichkeiten verspricht, kann unter Umständen ein Sicherheitsrisiko sein. Die unbesehene Ausführung von Code kann potentiell erheblichen Schaden anrichten, da es aus einem Python-Programm heraus durchaus möglich ist, zum Beispiel Dateien zu löschen.

Sehen wir uns an, wie die input()-Funktion für eine alternative Implementierung unseres Beispielprogramms genutzt werden kann.

1# beispiel_2.py
2str = input('auszugebender Text: ')
3nmax = int(input('Anzahl der Wiederholungen: '))
4for n in range(nmax):
5    print(str)

Nach dem Aufruf des Programms werden nun die Parameter abgefragt, wobei die Anzahl der Wiederholungen wiederum in einen Integer umgewandelt werden muss. Anschließend erfolgt die mehrfache Ausgabe des eingegebenen Textes.

$ python beispiel_2.py
auszugebender Text: Hallo
Anzahl der Wiederholungen: 3
Hallo
Hallo
Hallo

Wir haben uns hier darauf beschränkt, das grundsätzliche Vorgehen zu demonstrieren. In der Praxis wäre es natürlich in beiden Beispielen sinnvoll, Fehler abzufangen, zum Beispiel für den Fall, dass sich die eingegebene Anzahl der Wiederholungen nicht in einen Integer umwandeln lässt.

Weiterführender Hinweis

Möchte man Parameter nicht auf der Kommandozeile oder allgemein in einem Terminalfenster eingeben, sondern über eine graphische Benutzeroberfläche, so kann man sich in Python Unterstützung in der Standardbibliothek in Form des tkinter-Moduls holen.

Nachdem wir uns bis jetzt auf die Eingabe konzentriert haben, wollen wir uns abschließend noch kurz der Ausgabe mit Hilfe der print()-Funktion zuwenden. Diese Funktion haben wir schon bei verschiedensten Gelegenheiten verwendet. Im Kapitel 3.7 hatten wir unter anderem auch gesehen, dass der standardmäßige Zeilenumbruch am Ende der Ausgabe, durch den Parameter end durch eine andere Ausgabe ersetzt werden kann.

Wir wollen die print-Funktion nun nutzen, um einen allgemeinen Aspekt der Ausgabe von Daten zu besprechen, der in der Praxis gelegentlich wichtig sein kann. Ein- und Ausgabeoperationen sind typischerweise sehr langsam im Vergleich zu Rechenoperationen im Prozessor. Jede einzelne Ein- oder Ausgabeoperation sofort auszuführen, würde daher die Abarbeitung des Programms unnötig verzögern. Besser ist es zum Beispiel bei der Ausgabe von Daten, eine gewisse Datenmenge in einem Puffer anzusammeln und dann den gesamten Datenblock auszugeben. Praktisch bedeutet dies, dass man sich nicht darauf verlassen kann, dass die von einer print-Anweisung angestoßene Ausgabeoperation sofort vollständig ausgeführt wird. Für die print-Anweisung kann man eine sofortige Ausführung jedoch erzwingen, indem man das Argument flush auf True setzt.

Da wir es hier mit zeitlichen Abläufen zu tun haben, illustrieren wir die Auswirkung des Pufferns in einem Film. Wir verwenden dazu die print-Anweisung, aber die Pufferung von Daten wird auch im nächsten Abschnitt noch einmal eine Rolle spielen, wenn wir die Ausgabe von Daten in eine Datei besprechen.

7.2. Lesen von Dateien#

Für einfache wissenschaftliche Problemstellungen mag es ausreichen, Parameter über die Kommandozeile oder auf Anfrage des Programms einzugeben und die resultierenden Daten am Bildschirm anzusehen. Oft wird es aber so sein, dass dieses Vorgehen aufgrund des Umfangs der Daten nicht mehr sinnvoll ist. So kann es bereits zu umständlich sein, zehn oder zwanzig Parameter immer wieder per Hand einzugeben und in manchen Fällen kann die Zahl der Eingabeparameter deutlich größer sein. Man denke zum Beispiel an Strukturdaten für quantenchemische Rechnungen. Die erzeugten Daten sind häufig sehr umfangreich, so dass man sie für eine weitere Analyse abspeichern möchte. Dies gilt besonders dann, wenn die Erzeugung der Daten sehr zeitaufwändig ist.

Wir wollen uns zunächst dem Lesen von Daten aus Dateien zuwenden. Das grundsätzliche Vorgehen ähnelt dem beim Lesen eines Buches. So wie man das Buch zum Lesen aufschlagen muss, muss man eine Datei zum Lesen zunächst öffnen. Anschließend kann man im Buch lesen, wobei wir uns hier auf die Situation beschränken wollen, in der beginnend am Anfang gelesen wird. Wie in einem Buch ist es zwar im Prinzip auch in einer Datei möglich, direkt zu einer bestimmten Stelle zu springen, aber dies kommt in der Praxis vor allem im Zusammenhang mit binären Dateien vor. Hier wollen wir uns dagegen auf Textdateien beschränken, wobei Text nicht ausschließt, dass die Datei ausschließlich oder zum Teil numerische Information enthält.

Genauso wie man ein Buch, nachdem man Teile oder den gesamten Inhalt gelesen hat, wieder zuschlägt, sollte man auch eine Datei schließen. Im Prinzip wird dies von modernen Betriebssystemen notfalls erledigt, aber es ist keine gute Praxis darauf zu hoffen, dass jemand anderes für einen die Aufräumarbeit erledigt. Problematisch würde dies vor allem, wenn man viele Bücher gleichzeitig aufschlägt oder viele Dateien gleichzeitig öffnet.

Um eine Datei lesen zu können, muss diese Datei zunächst einmal existieren. Wir gehen im Folgenden davon aus, dass es eine Datei mit dem Namen foo.dat im aktuellen Verzeichnis gibt. Den Inhalt dieser Datei lassen wir uns hier mit einem sogenannten magischen Befehl von Jupyter ausgeben.

%cat foo.dat
 1.37  2.59
10.3  -1.3
 5.8   2.0

Dann können wir diese Datei zum Lesen öffnen. Dabei erhalten wir ein Dateiobjekt zurück, mit dem wir anschließend auf den Inhalt der Datei zugreifen können.

datei = open('foo.dat')
print(datei)
<_io.TextIOWrapper name='foo.dat' mode='r' encoding='UTF-8'>

Die print-Anweisung gibt hier nicht den Inhalt der Datei aus, die ja noch überhaupt nicht gelesen wurde, sondern Information über das Dateiobjekt, das durch die Variable datei repräsentiert wird. Wie wir sehen, erlaubt uns das Dateiobjekt tatsächlich Zugriff auf unsere Datei foo.dat.

Der Zugriffsmodus ist r, was als Abkürzung für read andeutet, dass die Datei zum Lesen geöffnet ist. Später werden wir noch andere Werte für den Zugriffsmodus kennenlernen. Möchte man an dieser Stelle betonen, dass die Datei nur zum Lesen geöffnet werden soll, kann man dies mit Hilfe des mode-Arguments in der open-Anweisung tun. Da der Default aber der Lesezugriff ist, ist dies nicht unbedingt erforderlich.

Schließlich ist noch die Textkodierung festgelegt. Standardmäßig wird die vom Betriebssystem bevorzugte Kodierung verwendet, die in unserem Fall die UTF-8-Kodierung ist, wie es heutzutage auf den meisten Systemen der Fall sein dürfte. Sollte der zu lesende Text in einer anderen Kodierung vorliegen, so muss man das encoding-Argument in der open-Anweisung entsprechend setzen.

Der Versuch, eine Datei zum Lesen zu öffnen, die überhaupt nicht existiert, führt zu einem FileNotFoundError.

open('nonexistent.dat')
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[3], line 1
----> 1 open('nonexistent.dat')

File /usr/share/miniconda/lib/python3.11/site-packages/IPython/core/interactiveshell.py:324, in _modified_open(file, *args, **kwargs)
    317 if file in {0, 1, 2}:
    318     raise ValueError(
    319         f"IPython won't let you open fd={file} by default "
    320         "as it is likely to crash IPython. If you know what you are doing, "
    321         "you can use builtins' open."
    322     )
--> 324 return io_open(file, *args, **kwargs)

FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent.dat'

Wir wollen nun aber das bereits zuvor erzeugte Dateiobjekt datei nutzen, um die Datei foo.dat zu lesen. Angesichts des in modernen Rechnern zur Verfügung stehenden großen Hauptspeichers ist es in den meisten Fällen möglich, den Inhalt der gesamten Datei in diesen Speicher zu laden. Dies lässt sich in Python auf zwei Arten bewerkstelligen. Zunächst verwenden wir die read-Methode des Dateiobjekts.

inhalt = datei.read()
inhalt
' 1.37  2.59\n10.3  -1.3\n 5.8   2.0\n'

Wie wir sehen, wird der gesamte Inhalt in eine Zeichenkette geladen, wobei die Zeilenumbrüche jeweils an dem Steuerzeichen \n erkennbar sind. Um diese Steuerzeichen deutlich zu machen, haben wir hier nicht die print()-Funktion zu Ausgabe verwendet.

Eine bequeme Methode, um diese Zeichenkette in einzelnen Zeilen zu zerlegen stellt die splitlines()-Methode bereit.

print(inhalt.splitlines())
[' 1.37  2.59', '10.3  -1.3', ' 5.8   2.0']

Wir erhalten damit eine Liste, die die einzelnen Zeilen als Einträge enthält.

Was passiert nun, wenn wir versuchen, die Datei ein zweites Mal zu lesen?

datei.read()
''

Das Ergebnis ist nun eine leere Zeichenkette. Wie lässt es sich erklären, dass wir nicht unser voriges Ergebnis reproduzieren können? Man kann sich den Leseprozess am besten veranschaulichen, wenn man sich das Lesen der Datei von einem historischen Datenspeicher, einem Magnetband, vorstellt. Dort bewegt sich ein Lesekopf entlang des Magnetbandes. Ganz entsprechend gibt es auch heute noch einen Zeiger, der auf die aktuelle Position in der Datei verweist. Zu Beginn des Leseprozesses steht dieser Zeiger am Beginn der Datei und nach dem Lesen an deren Ende. Versucht man dann weiterzulesen, so erhält man keine Daten mehr. Im Prinzip kann man den Zeiger zwar beliebig in der Datei neu positionieren, aber diese Möglichkeiten werden bei Textdateien eigentlich nicht benötigt, da wir ja die gesamte Datei bereits eingelesen haben und damit arbeiten können.

Da wir noch weitere Möglichkeiten demonstrieren wollen, Daten einzulesen, schließen wir zunächst die Datei wieder um sie dann erneut zu öffnen.

datei.close()
print(datei.closed)
True

Mit der ersten Zeile schließen wir die Datei. In der zweiten Zeile haben wir dann zur Illustration abgeprüft, ob die Datei wirklich geschlossen ist.

Vor allem bei größeren Dateien möchte man vielleicht nicht die gesamte Datei auf einmal laden, sondern diese zeilenweise lesen und verarbeiten. Iteriert man in einer for-Schleife über das Dateiobjekt, so erhält man die einzelnen Zeilen.

datei = open('foo.dat')
for zeile in datei:
    print(zeile)
datei.close()
 1.37  2.59

10.3  -1.3

 5.8   2.0

An den ausgegebenen Leerzeilen erkennen wir, dass das Zeilenumbruchzeichen am Ende der Zeile nicht entfernt wurde.

Um das Schließen der Datei unter allen Umständen, also selbst im Fehlerfall, sicherzustellen, bedient man sich in Python normalerweise eines Kontext-Managers. Das vorige Beispiel lässt sich dann folgendermaßen formulieren.

with open('foo.dat') as datei:
    for zeile in datei:
        print(zeile)
 1.37  2.59

10.3  -1.3

 5.8   2.0

Wir erkennen die gewohnte Struktur mit einem Schlüsselwort, das hier with lautet, und einem Doppelpunkt am Ende der Zeile. Der darauf folgende eingerückte Block läuft unter Kontrolle des Kontext-Managers, und es ist sichergestellt, dass am Ende des Blocks die Datei geschlossen wird. Neu ist in der erste Zeile die Konstruktion, die das Ergebnis der open-Anweisung mit Hilfe des Schlüsselworts as der Variablen datei zuweist.

In unserem konkreten Beispiel möchte man sicherlich auf die einzelnen Gleitkommawerte separat zugreifen, so dass man in der Praxis zunächst einmal die split()-Methode auf jede Zeile anwendet. Dies funktioniert hier ohne Angabe eines Arguments, da die Trennung dann an white space, also insbesondere Leerzeichen oder Tabulatorzeichen erfolgt. Zudem werden solche Zeichen vollständig entfernt, und das schließt auch das Steuerzeichen für den Zeilenumbruch mit ein.

with open('foo.dat') as datei:
    for zeile in datei:
        print(zeile.split())
['1.37', '2.59']
['10.3', '-1.3']
['5.8', '2.0']

Allerdings zeigt das Ergebnis, dass beim Einlesen zunächst einmal ausschließlich Zeichenketten vorliegen. Es ist hier also noch erforderlich, die einzelnen Zeilenbestandteile in den richtigen Datentyp umzuwandeln, hier also in Gleitkommazahlen. Natürlich könnte man dazu über jede einzelne Liste iterieren und nach der Umwandlung mit der float()-Funktion neue Listen aufbauen. In Python lässt sich das leichter und übersichtlicher mit der map()-Funktion erledigen, wobei die folgende for-Schleife lediglich zur Ausgabe dient.

data = map(float, ['1.37', '2.59'])
for d in data:
    print(d, type(d))
1.37 <class 'float'>
2.59 <class 'float'>

Im Prinzip könnte man ähnlich vorgehen, wenn die einzelnen Einträge in einer Zeile durch Kommas oder Semikolons getrennt sind. Solche Dateien begegnen einem in der Praxis häufig, wenn Daten mit Excel erfasst wurden und dann im CSV-Format abgespeichert wurden, wobei die Abkürzung CSV für comma separated values steht. Anstatt das Einlesen selbst zu programmieren, bietet es sich dann an, auf Funktionen aus der Python-Standardbibliothek zurückzugreifen, in diesem Fall konkret auf das csv-Modul. Eine Programmbibliothek, die diverse Eingabeformate einlesen kann und für das Verarbeiten von strukturierten Daten in Python besonders gut geeignet ist, ist Pandas.

Weiterführender Hinweis

Große Mengen strukturierter Daten werden heute häufig im HDF5-Format gespeichert. Dieses kann von Pandas gelesen werden. Wenn man Pandas jedoch nicht zur Weiterverarbeitung der Daten verwenden möchte, ist es sinnvoll, sich das h5py-Paket anzusehen.

Anstatt Parameter wie in Kapitel 7.1 beschrieben an ein Programm zu übergeben, werden auch Konfigurationsdateien verwendet, wie man sie als INI-Dateien in Windows kennt. Zum Lesen solcher Dateien stellt die Python-Standardbibliothek das configparser-Modul zur Verfügung.

Beliebte Formate für den Datenaustausch sind unter anderem XML (eXtensible Markup Language) und JSON (JavaScript Object Notation). Auch diese unterstützt Python in der Standardbibliothek mit einer Reihe von XML verarbeitenden Modulen beziehungsweise dem json-Modul.

Die genannten Pakete unterstützen nicht nur das Lesen der genannten Dateitypen, sondern auch das Schreiben.

7.3. Schreiben von Dateien#

Genauso wie für das Lesen aus Dateien muss man zum Schreiben in eine Datei diese zunächst öffnen. Hat man das Schreiben beendet, so sollte die Datei wieder geschlossen werden. In Python macht man dies am Einfachsten im Rahmen eines with-Kontexts, wie wir ihn schon beim Lesen aus Dateien kennengelernt haben.

Im Kapitel 7.2 hatten wir gesehen, dass eine Datei defaultmäßig im Modus r, also zum Lesen, geöffnet wird. In diesem Modus ist es nicht möglich, in die geöffnete Datei zu schreiben. Statt der read()-Methode zum Lesen müssen wir hier die write()-Methode zum Schreiben verwenden, um das Verhalten zu demonstrieren.

with open('foo.txt') as datei:
    datei.write('Dies ist ein Test.')
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[12], line 1
----> 1 with open('foo.txt') as datei:
      2     datei.write('Dies ist ein Test.')

File /usr/share/miniconda/lib/python3.11/site-packages/IPython/core/interactiveshell.py:324, in _modified_open(file, *args, **kwargs)
    317 if file in {0, 1, 2}:
    318     raise ValueError(
    319         f"IPython won't let you open fd={file} by default "
    320         "as it is likely to crash IPython. If you know what you are doing, "
    321         "you can use builtins' open."
    322     )
--> 324 return io_open(file, *args, **kwargs)

FileNotFoundError: [Errno 2] No such file or directory: 'foo.txt'

Wenn wir dagegen die Datei im Schreibmodus w öffnen, können wir die Datei wie gewünscht schreiben.

with open('foo.txt', mode='w') as datei:
    datei.write('Dies ist ein Test.')

Das Ergebnis können wir uns wie in Kapitel 7.2 mit dem magischen Befehl %cat in einer Notebook-Zelle ansehen.

%cat foo.txt
Dies ist ein Test.

Im Zusammenhang mit der write()-Methode ist allerdings zu beachten, dass diese im Gegensatz zur print()-Funktion nicht automatisch einen Zeilenumbruch anhängt. Im folgenden Beispiel verzichten wir darauf, beim zweiten Argument das Schlüsselwort mode anzugeben, da dieses Argument an der richtigen Position steht. Es spricht aber natürlich nichts dagegen, das Schlüsselwort zur Verdeutlichung anzugeben.

with open('foo.txt', 'w') as datei:
    for n in range(1, 4):
        datei.write(f'Zeile {n}')
%cat foo.txt
Zeile 1Zeile 2Zeile 3

Dieses Verhalten entspricht dem, was wir von der print()-Funktion kennen, wenn wir end='' setzen. Wollen wir einen Zeilenumbruch erreichen, so müssen wir das entsprechende Steuerzeichen explizit angeben.

with open('foo.txt', 'w') as datei:
    for n in range(1, 4):
        datei.write(f'Zeile {n}\n')
%cat foo.txt
Zeile 1
Zeile 2
Zeile 3

Die Möglichkeiten der Formatierung in f-Strings hatten wir in einigem Detail in Kapitel 3.7 besprochen, auf das wir an dieser Stelle verweisen wollen.

Benutzt man den Modus w zum Öffnen einer Datei zum Schreiben, muss man sich bewusst sein, dass eine eventuell bereits existierende Datei mit diesem Namen zunächst gelöscht wird. Dies gilt zumindest, wenn man auf der Ebene des Betriebssystems die Rechte dazu besitzt. Je nach Situation kann dies das erwünschte Verhalten sein oder es stört einen zumindest nicht. Es gibt jedoch Anwendungen, in denen man einen alternativen Modus verwenden wird.

Statt des Modus w kann man den Modus x verwenden, der die Datei nur dann zum Schreiben öffnen wird, wenn eine Datei mit dem betreffenden Namen noch nicht existiert. Dies können wir anhand der Datei foo.txt demonstrieren, die wir ja gerade geschrieben hatten.

with open('foo.txt', 'x') as datei:
    for n in range(1, 4):
        datei.write(f'Zeile {n}\n')
---------------------------------------------------------------------------
FileExistsError                           Traceback (most recent call last)
Cell In[19], line 1
----> 1 with open('foo.txt', 'x') as datei:
      2     for n in range(1, 4):
      3         datei.write(f'Zeile {n}\n')

File /usr/share/miniconda/lib/python3.11/site-packages/IPython/core/interactiveshell.py:324, in _modified_open(file, *args, **kwargs)
    317 if file in {0, 1, 2}:
    318     raise ValueError(
    319         f"IPython won't let you open fd={file} by default "
    320         "as it is likely to crash IPython. If you know what you are doing, "
    321         "you can use builtins' open."
    322     )
--> 324 return io_open(file, *args, **kwargs)

FileExistsError: [Errno 17] File exists: 'foo.txt'

Der Versuch, in eine existierende Datei zu schreiben, wird also beim Modus x unterbunden.

Es kann aber auch vorkommen, dass man in einer bereits existierenden Datei am Ende der Datei weiterschreiben möchte. Hierzu ist der Modus a für append vorgesehen. Ein Anwendungsfall kann darin bestehen, Probleme durch das Puffern der Ausgabe, die wir am Ende von Kapitel 7.1 besprochen hatten, zu umgehen. So könnte man bei einem Programm mit langer Laufzeit, bei dem in größeren Abständen Daten geschrieben werden, die Datei nur unmittelbar zum Schreiben der Daten wieder öffnen und anschließend wieder schließen. Bei einem Programmabbruch gehen damit die bereits ausgegebenen Daten nicht verloren. Dieses Vorgehen ist allerdings nur dann sinnvoll, wenn die Zeiten zwischen den Schreibvorgängen nicht zu kurz sind, da sonst der Aufwand für das Öffnen und Schließen der Datei das Programm ausbremsen würde.

Im folgenden Beispiel demonstrieren wir mit Hilfe einer Schleife außerhalb des with-Kontexts das wiederholte Anhängen an eine Datei. Zudem überprüfen wir, dass die Datei jeweils wieder geschlossen wurde.

from datetime import datetime
from time import sleep

for n in range(1, 5):
    sleep(5)
    now = datetime.now()
    with open('spam.dat', 'a') as datei:
        msg = f'{now:%H:%M:%S} - Durchlauf {n}\n'
        datei.write(msg)
    if datei.closed:
        msg = f'{now:%H:%M:%S} - Datei geschlossen'
        print(msg)
14:57:45 - Datei geschlossen
14:57:50 - Datei geschlossen
14:57:55 - Datei geschlossen
14:58:00 - Datei geschlossen
%cat spam.dat
14:57:45 - Durchlauf 1
14:57:50 - Durchlauf 2
14:57:55 - Durchlauf 3
14:58:00 - Durchlauf 4

Möchte man mehrere Dateien schreiben, weil man beispielsweise Rechnungen für mehrere Parametersätze durchführen möchte, sollte man im Hinterkopf behalten, dass es sich bei dem Namen, der beim Öffnen der Datei angegeben werden muss, einfach um eine Zeichenkette handelt, die entsprechend konstruiert werden kann. So hat man die Möglichkeit, entweder Parameter im Dateinamen unterzubringen oder die Dateien durchzunummerieren. Wir wollen letzteres an einem Beispiel demonstrieren.

for n in range(1, 16):
    with open(f'mydata_{n:04}.dat', 'w') as datei:
        datei.write(f'Datei Nr. {n}\n')
%ls mydata*.dat
mydata_0001.dat  mydata_0005.dat  mydata_0009.dat  mydata_0013.dat
mydata_0002.dat  mydata_0006.dat  mydata_0010.dat  mydata_0014.dat
mydata_0003.dat  mydata_0007.dat  mydata_0011.dat  mydata_0015.dat
mydata_0004.dat  mydata_0008.dat  mydata_0012.dat

Um eine übersichtliche Sortierung der Dateien zu erzeugen, ist es sinnvoll, für die Nummer der Datei ein hinreichend breites Feld vorzusehen und die freien Stellen mit Nullen zu füllen.

Ganz unabhängig davon, wie man den Dateinamen wählt, ist es sinnvoll, Informationen, die erforderlich sind um die Daten zu erzeugen, zu Beginn der Datei abzuspeichern. Dazu gehören nicht nur die verwendeten Parameter, sondern auch Information über die verwendete Programmversion. Damit lässt sich im Fall eines Fehlers im Programm auch im Nachhinein entscheiden, ob die Daten hiervon betroffen sind.