26. Dateien

Was ist, wenn wir die die Grammatik ändern wollen? Wir müssen jedes Mal den Quellcode des Programms ändern. Außerdem ist das Format nicht sehr eingabefreundlich. Wir möchten lieber etwas wie

S -> b
S -> aSa

in eine Datei (ein file) schreiben, die das Programm dann liest.

Dateien sind in Python Objekte, die von der eingebauten Funktion open erzeugt werden:

grammarFile = open("grammar")

In modernen Python-Versionen kann man Dateien auch über die “eingebaute Funktion” file erzeugen. In gewisser Weise ist dies besser, weil es analog zu int, str und Freunden funktioniert: Es konstruiert einen Wert mit dem durch seinen Namen gegebenen Typ aus etwas anderem (im Fall von file ist dieses andere praktisch immer ein String mit dem Dateinamen).

Andererseits können alte Python-Versionen nichts damit anfangen (auch wenn man das mit file = open beheben könnte), und die meisten Leute verwenden immer noch open, so dass man wohl vorerst noch bei open bleiben sollte.

Dateien sind eigentlich auch nur eine Datenstruktur – sie unterscheidet sich allerdings von allem, was wir bisher kennen, in einem Punkt: Man kann nicht ohne weiteres auf beliebige Elemente innerhalb der Datei zugreifen. Tatsächlich steht in dem, was wir von open zurückbekommen (das file-Objekt) nicht der Inhalt der Datei, sondern nur etwas, das einem den Zugriff auf die wirklichen Daten erlaubt, die weiter (etwa) auf der Platte liegen.

Der Grund dafür ist, dass Massenspeicher und ähnliche Geräte, die man mit Dateien abstrahiert, anders gebaut sind als der Hauptspeicher, in dem Listen, Dictionaries usf liegen. Die heute üblichen Festplatten beispielsweise sind als rotierende Scheiben realisert, über denen je ein Schreib-/Lesekopf schwebt. Um ein Byte zu lesen, muss man den Kopf zunächst in die richtige Spur bewegen und dann warten, bis es durch die Rotation vorbeiflitzt. Weil das ziemlich aufwändig ist, ist der Zugriff auf Daten am Massenspeicher tief unten an der Hardware so geregelt, dass man immer nur einen ganzen Haufen Zeichen (ein paar hundert oder tausend davon) auf einmal lesen oder schreiben kann. Besonders gut ist, wenn man die Daten in der Reihenfolge haben will, in der sie auf der Platte stehen. Python und das Betriebssystem verstecken einen Teil dieser Schwierigkeiten, trotzdem muss man mit Dateien anders umgehen als, sagen wir, mit Listen.

Der Fall, dass der Zugriff auf Daten mit relativ großem Aufwand verbunden ist und es noch dazu eine Art natürliche Reihenfolge des Lesens oder Schreibens gibt, ist gar nicht so selten – liest man etwa Daten vom Netz oder schreibt sie in einen Digital-Analog-Wandler, der Töne daraus macht, sehen die Verhältnisse sehr ähnlich aus, und in der Tat werden auch in diesen Fällen die wirklichen Vorgänge hinter Dateien oder Objekten, die ganz ähnlich funktionieren, versteckt.

Ein paar Methoden von Dateien:

  • read([size]) – Liest den ganzen Inhalt der Datei (oder nur size Bytes) in einen String, der dann zurückkommt.
  • readline – Liest die nächste Zeile aus der Datei und gibt sie in einem String zurück. Wenn nichts mehr in der Datei steht, kommt ein leerer String zurück.
  • readlines – Gibt alle Zeilen der Datei als Liste zurück
  • close – Schließt die Datei

Damit könnten wir unsere Grammatik so einlesen:

def readRules(inFileName):
  rules = {}
  rulesFile = open(inFileName)
  rawRules = rulesFile.readlines()
  rulesFile.close()
  for rawRule in rawRules:
    leftRight = rawRule.split("->")
    if len(leftRight)==2:
      key = leftRight[0].strip()
      val = leftRight[1].strip()
      rules.setdefault(key, []).append(val)
  return rules

Anmerkungen:

  • Die Funktion legt als erstes die Datenstruktur an, die sie nachher zurückgibt. In statischeren Sprachen übergibt man häufig eine Datenstruktur, die die Funktion dann füllen soll, in Python mit seiner dynamischen Speicherverwaltung ist das in der Regel schlechter Stil.
  • Wenn man eine Datei schließt, werden verschiedene Verwaltungsstrukturen freigegeben. Dateien sollten immer so bald wie möglich geschlossen werden, um nicht zu viele dieser Verwaltungsstrukturen zu belegen.
  • (Weiterführend:) In der gegenwärtigen Python-Implementation ist das aber häufig kein großes Problem, weil Python die Datei selbstständig schließt, sobald wir keine Referenz mehr darauf haben. Wenn nun Dateien in Funktionen behandelt werden, wird die Datei automatisch geschlossen, wenn wir die Funktion verlassen, in ihr definierte Variablen also vergessen werden.
  • (Weiterführend:) Dieser Rechnung folgend hätten wir hier auch rawRules = open("grammar").readlines() schreiben können; die Datei wäre dann schon nach dieser Zeile (genauer: vor der Zuweisung) geschlossen worden, weil nach der Anwendung der readlines-Methode keine Referenz mehr auf das Ergebnis von open existiert. Leider garantiert die Python-Spezifikation dieses Verhalten nicht, und in der Tat kann es bei der Verwendung des Java-basierten Python Probleme geben. Deshalb ist es (insbesondere für Schreibzugriffe) besser, Dateien explizit zu schließen, auch wenn wir hier sehr häufig das Idiom open(...).readX() verwenden werden.
  • Die strip-Methode, die wir auf das Ergebnis des split anwenden, ist nötig, um Leerzeichen und Zeilenvorschübe zu entfernen.
  • Die Kondition auf die Länge von leftRight dient dazu, leere Zeilen (allgemeiner: Zeilen, die unserer Syntax nicht entsprechen) sicher überlesen zu können. In einem realen Programm würde man sicher eine elaboriertere Behandlung von Formatfehlern in der Eingabe vorsehen wollen.

In Dateien kann man auch schreiben. Man muss sie dann mit open(<dateiname>, "w") (für “write”) erzeugen, danach kann man per write reinschreiben:

>>> f = open("test", "w")
>>> f.write("Hallo Welt\n")
>>> f.close()
>>> ^D
tucana:examples> cat test
Hallo Welt

Dateien und Encoding

Aus Dateien bekommt ihr durchweg Bytestrings zurück, für die Interpretation der Bytes seid ihr selbst verantwortlich. Analog könnt ihr in Dateien nur Bytestrings schreiben, der Versuch, Unicode-Strings zu schreiben, scheitert, sobald Nicht-ASCII-Zeichen im String sind.

Das ist natürlich lästig, wenn man Sprache verarbeiten möchte, aber nicht zu vermeiden, weil kein mir bekanntes Betriebssystem zu einer Textdatei das von dem schreibenden Programm verwendete Encoding speichert (so ein Datensatz wäre ein Beispiel für so genannte Metadaten, also Daten über Daten – Metadaten, die übliche Betriebssysteme speichern, sind beispielsweise Zugriffszeiten, Eigentümer von Dateien usf.). Verschiedene Kommunikationsprotokolle (z.B. http oder auch das für E-Mail entwickelte MIME) erlauben übrigens, auf Seitenkanälen Information über das Encoding auszutauschen, aber das hilft genau dann, wenn man mit entsprechenden Daten umgeht. Dokumentation dazu findet ihr in den entsprechenden Modulen.

Das bedeutet: Wenn euer Programm Dateien verwendet und ihr Unicode-Strings verwenden wollt, müsst ihr ein Encoding vereinbaren und ensprechend die Strings, die von read() zurückkommen, Dekodieren und zum Schreiben wieder Enkodieren. Im codecs-Modul ist übrigens eine Funktion open enthalten, die File-Objekte zurückgeben, die diese Kodieren und Dekodieren selbst machen. Wir werden im Zusammenhang mit Vererbung sehen, wie man sowas selbst schreiben kann.

Solange ihr aber mit Bytestrings auskommt (das könnt ihr meist, solange ihr euch auf einem Rechner bewegt und lediglich westeuropäische Sprachen verarbeitet) und euer Programm das gleiche Encoding verwendet wie eure Daten, müsst ihr euch um diese Dinge noch keine Gedanken machen.

Übungen zu diesem Abschnitt

Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen

(1)

Schreibt eine Funktion catUpper(fName) -> None, die den Inhalt der durch fName bezeichneten Datei in Großbuchstaben ausgibt. Wenn ihr keinen anderen Dateinamen zum Probieren wisst, tut es auch der Name des Skripts, in das ihr die Funktion schreibt. Wenn das so ist, solltet ihr euch allerdings schleunigst mit dem Dateisystem eures Betriebssystems auseinandersetzen.

(2)

readline

und Freunde funktionieren nur für Textdateien, d.h. Dateien, in denen auch wirklich Text steht. Für Binärdateien (also solche, die nicht aus Zeilen mit harmlosen Text bestehen) kommen unter Umständen sehr komische Dinge heraus. Probiert mal, was folgendes tut:

>>> f = open("...(Pfad zu Binärdatei)...")
>>> f.readline()
>>> f.readline()
...

Unter Unix empfehle ich als Binärdatei etwas wie /bin/cat oder /lib/libc.so, unter Windows ist die Verwendung einer Word-Datei recht instruktiv.

(3)

Schreibt eine Funktion filecopy(fName1, fName2) -> None, die eine Textdatei fName1 nach fName2 kopiert und dabei nicht allzu viel Hauptspeicher braucht.


Markus Demleitner

Copyright Notice