Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 3116 – Neues I/O

Autor:
Daniel Stutzbach <daniel at stutzbachenterprises.com>, Guido van Rossum <guido at python.org>, Mike Verdone <mike.verdone at gmail.com>
Status:
Final
Typ:
Standards Track
Erstellt:
26. Februar 2007
Python-Version:
3.0
Post-History:
26. Februar 2007

Inhaltsverzeichnis

Begründung und Ziele

Python ermöglicht eine Vielzahl von stream-ähnlichen (auch datei-ähnlichen) Objekten, die über read()- und write()-Aufrufe verwendet werden können. Alles, was read() und write() bereitstellt, ist stream-ähnlich. Jedoch sind exotischere und äußerst nützliche Funktionen wie readline() oder seek() möglicherweise nicht auf jedem stream-ähnlichen Objekt verfügbar. Python benötigt eine Spezifikation für grundlegende Byte-basierte I/O-Streams, zu denen wir Pufferungs- und Textbehandlungsfunktionen hinzufügen können.

Sobald wir eine definierte rohe Byte-basierte I/O-Schnittstelle haben, können wir Pufferungs- und Textbehandlungsschichten auf jeder Byte-basierten I/O-Klasse hinzufügen. Die gleiche Pufferungs- und Textbehandlungslogik kann für Dateien, Sockets, Byte-Arrays oder benutzerdefinierte I/O-Klassen, die von Python-Programmierern entwickelt wurden, verwendet werden. Die Entwicklung einer Standarddefinition eines Streams ermöglicht es uns, Stream-basierte Operationen wie read() und write() von implementierungsspezifischen Operationen wie fileno() und isatty() zu trennen. Dies ermutigt Programmierer, Code zu schreiben, der Streams als Streams verwendet und nicht verlangt, dass alle Streams dateispezifische oder sockel-spezifische Operationen unterstützen.

Die neue I/O-Spezifikation ist ähnlich den Java I/O-Bibliotheken gedacht, aber generell weniger verwirrend. Programmierer, die sich nicht mit der neuen I/O-Welt auseinandersetzen wollen, können erwarten, dass die Factory-Methode open() ein Objekt zurückgibt, das abwärtskompatibel zu alten Dateiobjekten ist.

Spezifikation

Die Python I/O-Bibliothek wird aus drei Schichten bestehen: einer rohen I/O-Schicht, einer gepufferten I/O-Schicht und einer Text-I/O-Schicht. Jede Schicht wird durch eine abstrakte Basisklasse definiert, die mehrere Implementierungen haben kann. Die rohen I/O- und gepufferten I/O-Schichten arbeiten mit Byte-Einheiten, während die Text-I/O-Schicht mit Zeichen-Einheiten arbeitet.

Rohes I/O

Die abstrakte Basisklasse für rohes I/O ist `RawIOBase`. Sie hat mehrere Methoden, die Wrapper um die entsprechenden Betriebssystemaufrufe sind. Wenn eine dieser Funktionen für das Objekt keinen Sinn ergibt, muss die Implementierung eine `IOError`-Ausnahme auslösen. Wenn beispielsweise eine Datei schreibgeschützt geöffnet ist, löst die Methode `.write()` eine `IOError` aus. Als weiteres Beispiel, wenn das Objekt einen Socket darstellt, lösen `.seek()`, `.tell()` und `.truncate()` eine `IOError` aus. Im Allgemeinen entspricht ein Aufruf einer dieser Funktionen genau einem Betriebssystemaufruf.

.read(n: int) -> bytes
Liest bis zu n Bytes aus dem Objekt und gibt diese zurück. Weniger als n Bytes können zurückgegeben werden, wenn der Betriebssystemaufruf weniger als n Bytes zurückgibt. Wenn 0 Bytes zurückgegeben werden, zeigt dies das Ende der Datei an. Wenn das Objekt im nicht-blockierenden Modus ist und keine Bytes verfügbar sind, gibt der Aufruf None zurück.

.readinto(b: bytes) -> int

Liest bis zu len(b) Bytes aus dem Objekt und speichert sie in b, wobei die Anzahl der gelesenen Bytes zurückgegeben wird. Ähnlich wie bei `.read()` können weniger als len(b) Bytes gelesen werden, und 0 zeigt das Ende der Datei an. None wird zurückgegeben, wenn ein nicht-blockierendes Objekt keine Bytes verfügbar hat. Die Länge von b wird nie geändert.

.write(b: bytes) -> int

Gibt die Anzahl der geschriebenen Bytes zurück, die kleiner sein kann als len(b).

.seek(pos: int, whence: int = 0) -> int

.tell() -> int

.truncate(n: int = None) -> int

.close() -> None

Zusätzlich definiert sie einige andere Methoden

.readable() -> bool
Gibt True zurück, wenn das Objekt zum Lesen geöffnet wurde, andernfalls False. Wenn False, löst `.read()` eine IOError aus, wenn es aufgerufen wird.

.writable() -> bool

Gibt True zurück, wenn das Objekt zum Schreiben geöffnet wurde, andernfalls False. Wenn False, lösen `.write()` und `.truncate()` eine IOError aus, wenn sie aufgerufen werden.

.seekable() -> bool

Gibt True zurück, wenn das Objekt zufälligen Zugriff unterstützt (wie Festplattendateien), oder False, wenn das Objekt nur sequenziellen Zugriff unterstützt (wie Sockets, Pipes und TTYs). Wenn False, lösen `.seek()`, `.tell()` und `.truncate()` eine IOError aus, wenn sie aufgerufen werden.

.__enter__() -> ContextManager

Kontextmanagement-Protokoll. Gibt self zurück.

.__exit__(...) -> None

Kontextmanagement-Protokoll. Gleicht `.close()`.

Wenn und nur wenn eine RawIOBase-Implementierung mit einem zugrunde liegenden Dateideskriptor arbeitet, muss sie zusätzlich eine `.fileno()`-Mitgliedsfunktion bereitstellen. Diese könnte spezifisch von der Implementierung definiert werden, oder eine Mix-in-Klasse könnte verwendet werden (muss darüber entschieden werden).

.fileno() -> int
Gibt den zugrunde liegenden Dateideskriptor zurück (eine Ganzzahl).

Anfänglich werden drei Implementierungen bereitgestellt, die die RawIOBase-Schnittstelle implementieren: FileIO, SocketIO (im Socket-Modul) und ByteIO. Jede Implementierung muss bestimmen, ob das Objekt zufälligen Zugriff unterstützt, da die vom Benutzer bereitgestellten Informationen möglicherweise nicht ausreichen (denken Sie an open("/dev/tty", "rw") oder open("/tmp/named-pipe", "rw")). Als Beispiel kann FileIO dies durch Aufruf des Systemaufrufs seek() ermitteln; wenn dieser einen Fehler zurückgibt, unterstützt das Objekt keinen zufälligen Zugriff. Jede Implementierung kann zusätzliche, für ihren Typ geeignete Methoden bereitstellen. Das ByteIO-Objekt ist analog zur cStringIO-Bibliothek von Python 2, arbeitet aber mit dem neuen Byte-Typ anstelle von Zeichenketten.

Gepuffertes I/O

Die nächste Schicht ist die gepufferte I/O-Schicht, die einen effizienteren Zugriff auf datei-ähnliche Objekte bietet. Die abstrakte Basisklasse für alle gepufferten I/O-Implementierungen ist BufferedIOBase, die ähnliche Methoden wie `RawIOBase` bereitstellt.

.read(n: int = -1) -> bytes
Gibt die nächsten n Bytes aus dem Objekt zurück. Sie kann weniger als n Bytes zurückgeben, wenn das Ende der Datei erreicht ist oder das Objekt nicht blockiert. 0 Bytes bedeuten das Ende der Datei. Diese Methode kann mehrere Aufrufe von RawIOBase.read() durchführen, um die Bytes zu sammeln, oder keine Aufrufe von RawIOBase.read() machen, wenn alle benötigten Bytes bereits gepuffert sind.

.readinto(b: bytes) -> int

.write(b: bytes) -> int

Schreibt b Bytes in den Puffer. Die Bytes werden nicht garantiert sofort an das Raw I/O-Objekt geschrieben; sie können gepuffert werden. Gibt len(b) zurück.

.seek(pos: int, whence: int = 0) -> int

.tell() -> int

.truncate(pos: int = None) -> int

.flush() -> None

.close() -> None

.readable() -> bool

.writable() -> bool

.seekable() -> bool

.__enter__() -> ContextManager

.__exit__(...) -> None

Zusätzlich bietet die abstrakte Basisklasse eine Mitgliedsvariable

.raw
Eine Referenz auf das zugrunde liegende RawIOBase-Objekt.

Die Methodensignaturen von BufferedIOBase sind größtenteils identisch mit denen von RawIOBase (Ausnahmen: `.write()` gibt `None` zurück, das Argument von `.read()` ist optional), können aber andere Semantiken haben. Insbesondere können BufferedIOBase-Implementierungen mehr Daten lesen als angefordert oder das Schreiben von Daten verzögern, indem sie Puffer verwenden. Für die meisten Zwecke ist dies für den Benutzer transparent (es sei denn, er öffnet beispielsweise dieselbe Datei über einen anderen Deskriptor). Darüber hinaus können rohe Lesevorgänge ohne besonderen Grund einen kurzen Lesevorgang zurückgeben; gepufferte Lesevorgänge geben einen kurzen Lesevorgang nur zurück, wenn das EOF erreicht ist; und rohe Schreibvorgänge können einen kurzen Zählwert zurückgeben (auch wenn nicht-blockierendes I/O nicht aktiviert ist!), während gepufferte Schreibvorgänge IOError auslösen, wenn nicht alle Bytes geschrieben oder gepuffert werden konnten.

Es gibt vier Implementierungen der BufferedIOBase-abstrakten Basisklasse, die unten beschrieben werden.

BufferedReader

Die Implementierung BufferedReader ist für sequenziell zugreifbare, nur-lesbare Objekte gedacht. Ihre `.flush()`-Methode ist eine No-Op.

BufferedWriter

Die Implementierung BufferedWriter ist für sequenziell zugreifbare, nur-schreibbare Objekte gedacht. Ihre `.flush()`-Methode erzwingt, dass alle zwischengespeicherten Daten an das zugrunde liegende `RawIOBase`-Objekt geschrieben werden.

BufferedRWPair

Die Implementierung BufferedRWPair ist für sequenziell zugreifbare Lese-/Schreibobjekte wie Sockets und TTYs gedacht. Da die Lese- und Schreibströme dieser Objekte vollständig unabhängig sind, könnte sie einfach durch die Einbeziehung einer BufferedReader- und einer BufferedWriter-Instanz implementiert werden. Sie bietet eine `.flush()`-Methode, die dieselbe Semantik hat wie die `.flush()`-Methode eines BufferedWriter.

BufferedRandom

Die Implementierung BufferedRandom ist für alle Objekte mit zufälligem Zugriff gedacht, unabhängig davon, ob sie nur lesbar, nur schreibbar oder lesbar/schreibbar sind. Im Vergleich zu den vorherigen Klassen, die auf sequenziell zugreifbaren Objekten arbeiten, muss die Klasse BufferedRandom damit umgehen, dass der Benutzer `.seek()` aufruft, um den Stream neu zu positionieren. Daher muss eine Instanz von BufferedRandom sowohl die logische als auch die tatsächliche Position innerhalb des Objekts verfolgen. Sie bietet eine `.flush()`-Methode, die alle zwischengespeicherten Schreibdaten an das zugrunde liegende RawIOBase-Objekt erzwingt und alle zwischengespeicherten Lesedaten verwirft (damit zukünftige Lesevorgänge gezwungen sind, auf die Festplatte zurückzugreifen).

F: Möchten wir in der Spezifikation vorschreiben, dass ein Wechsel zwischen Lesen und Schreiben auf einem Lese-/Schreibobjekt ein `.flush()` impliziert? Oder ist das ein Implementierungs-Convenience, auf das sich Benutzer nicht verlassen sollten?

Für ein nur-lesbares BufferedRandom-Objekt gibt `.writable()` False zurück und die Methoden `.write()` und `.truncate()` lösen IOError aus.

Für ein nur-schreibbares BufferedRandom-Objekt gibt `.readable()` False zurück und die Methode `.read()` löst IOError aus.

Text-I/O

Die Text-I/O-Schicht bietet Funktionen zum Lesen und Schreiben von Zeichenketten aus Streams. Einige neue Funktionen umfassen universelle Zeilenumbrüche sowie Zeichenkodierung und -dekodierung. Die Text-I/O-Schicht wird durch eine abstrakte Basisklasse TextIOBase definiert. Sie bietet mehrere Methoden, die den Methoden von BufferedIOBase ähneln, aber auf Zeichenbasis anstelle von Byte-Basis arbeiten. Diese Methoden sind:

.read(n: int = -1) -> str

.write(s: str) -> int

.tell() -> object

Gibt einen Cookie zurück, der die aktuelle Dateiposition beschreibt. Der einzige unterstützte Verwendungszweck für den Cookie ist mit `.seek()` mit `whence` auf 0 gesetzt (d.h. absoluter Sprung).

.seek(pos: object, whence: int = 0) -> int

Springt zur Position pos. Wenn pos nicht Null ist, muss es ein Cookie sein, der von `.tell()` zurückgegeben wurde, und whence muss Null sein.

.truncate(pos: object = None) -> int

Ähnlich wie BufferedIOBase.truncate(), mit der Ausnahme, dass pos (wenn nicht None) ein Cookie sein muss, der zuvor von `.tell()` zurückgegeben wurde.

Im Gegensatz zu rohem I/O sind die Einheiten für `.seek()` nicht spezifiziert – einige Implementierungen (z.B. StringIO) verwenden Zeichen und andere (z.B. TextIOWrapper) verwenden Bytes. Der Sonderfall für Null erlaubt es, zum Anfang oder Ende eines Streams zu gehen, ohne ein vorheriges `.tell()`. Eine Implementierung könnte den Stream-Encoder-Status in den von `.tell()` zurückgegebenen Cookie aufnehmen.

TextIOBase-Implementierungen stellen außerdem mehrere Methoden bereit, die Weiterleitungen an die zugrunde liegenden BufferedIOBase-Objekte sind.

.flush() -> None

.close() -> None

.readable() -> bool

.writable() -> bool

.seekable() -> bool

TextIOBase-Klassenimplementierungen stellen zusätzlich die folgenden Methoden bereit:

.readline() -> str
Liest bis zum Zeilenumbruch oder EOF und gibt die Zeile zurück, oder "", wenn EOF sofort erreicht wird.

.__iter__() -> Iterator

Gibt einen Iterator zurück, der Zeilen aus der Datei liefert (was zufällig self ist).

.next() -> str

Gleichbedeutend mit `readline()`, außer dass `StopIteration` ausgelöst wird, wenn EOF sofort erreicht wird.

Zwei Implementierungen werden von der Python-Bibliothek bereitgestellt. Die Hauptimplementierung, TextIOWrapper, umschließt ein gepuffertes I/O-Objekt. Jedes TextIOWrapper-Objekt hat eine Eigenschaft namens ".buffer", die eine Referenz auf das zugrunde liegende BufferedIOBase-Objekt bereitstellt. Seine Initialisierungssignatur lautet wie folgt:

.__init__(self, buffer, encoding=None, errors=None, newline=None, line_buffering=False)
buffer ist eine Referenz auf das BufferedIOBase-Objekt, das mit TextIOWrapper umschlossen werden soll.

encoding bezieht sich auf eine Kodierung, die zur Übersetzung zwischen der Byte-Darstellung und der Zeichen-Darstellung verwendet wird. Wenn sie None ist, wird die Systemeinstellung der Locale als Standard verwendet.

errors ist eine optionale Zeichenkette, die die Fehlerbehandlung angibt. Sie kann gesetzt werden, wenn encoding gesetzt werden kann. Standardmäßig ist sie 'strict'.

newline kann None, '', '\n', '\r' oder '\r\n' sein; alle anderen Werte sind ungültig. Sie steuert die Handhabung von Zeilenenden. Sie funktioniert wie folgt:

  • Beim Lesen, wenn newline None ist, ist der universelle Zeilenumbruchmodus aktiviert. Zeilen im Eingabetext können mit '\n', '\r' oder '\r\n' enden, und diese werden in '\n' übersetzt, bevor sie an den Aufrufer zurückgegeben werden. Wenn es '' ist, ist der universelle Zeilenumbruchmodus aktiviert, aber Zeilenenden werden unverändert an den Aufrufer zurückgegeben. Wenn es einen der anderen gültigen Werte hat, werden Eingabezeilen nur durch die angegebene Zeichenkette beendet, und das Zeilenende wird unverändert an den Aufrufer zurückgegeben. (Mit anderen Worten, die Übersetzung in '\n' erfolgt nur, wenn newline None ist.)
  • Beim Schreiben, wenn newline None ist, werden alle geschriebenen '\n'-Zeichen in den systemweiten Standard-Zeilentrenner, os.linesep, übersetzt. Wenn newline '' ist, findet keine Übersetzung statt. Wenn newline einer der anderen gültigen Werte ist, werden alle geschriebenen '\n'-Zeichen in die angegebene Zeichenkette übersetzt. (Beachten Sie, dass die Regeln für die Übersetzung beim Schreiben anders sind als beim Lesen.)

line_buffering, wenn auf True gesetzt, bewirkt, dass write()-Aufrufe ein flush() implizieren, wenn die geschriebene Zeichenkette mindestens einen '\n'- oder '\r'-Charakter enthält. Dies wird von open() gesetzt, wenn erkannt wird, dass der zugrunde liegende Stream ein TTY-Gerät ist, oder wenn ein buffering-Argument von 1 übergeben wird.

Weitere Hinweise zum Parameter newline

  • Die Unterstützung für '\r' ist für einige OSX-Anwendungen, die Dateien mit '\r'-Zeilenenden erzeugen, weiterhin erforderlich; Excel (beim Exportieren nach Text) und Adobe Illustrator EPS-Dateien sind die häufigsten Beispiele.
  • Wenn die Übersetzung aktiviert ist, geschieht dies unabhängig davon, welche Methode zum Lesen oder Schreiben aufgerufen wird. Zum Beispiel ergibt `f.read()` immer dasselbe Ergebnis wie `''.join(f.readlines())`.
  • Wenn universelle Zeilenumbrüche ohne Übersetzung beim Lesen angefordert werden (d.h. newline=''), und ein System-Leseaufruf einen Puffer zurückgibt, der mit '\r' endet, wird ein weiterer System-Leseaufruf durchgeführt, um festzustellen, ob dieser von '\n' gefolgt wird oder nicht. Im universellen Zeilenumbruchmodus mit Übersetzung kann der zweite System-Leseaufruf bis zum nächsten Leseaufruf verzögert werden, und wenn der nächste System-Leseaufruf einen Puffer zurückgibt, der mit '\n' beginnt, wird dieses Zeichen einfach verworfen.

Eine weitere Implementierung, StringIO, erstellt eine datei-ähnliche TextIO-Implementierung ohne zugrunde liegendes gepuffertes I/O-Objekt. Obwohl ähnliche Funktionalität durch das Umschließen eines BytesIO-Objekts in einem TextIOWrapper bereitgestellt werden könnte, ermöglicht das StringIO-Objekt eine deutlich höhere Effizienz, da es keine tatsächliche Kodierung und Dekodierung durchführen muss. Ein String-I/O-Objekt kann einfach die kodierte Zeichenkette unverändert speichern. Die __init__-Signatur des StringIO-Objekts nimmt eine optionale Zeichenkette für den Anfangswert entgegen; die Anfangsposition ist immer 0. Es unterstützt keine Kodierungen oder Zeilenumbruchübersetzungen; Sie lesen immer genau die Zeichen zurück, die Sie geschrieben haben.

Probleme bei Unicode-Kodierung/-Dekodierung

Wir sollten es ermöglichen, die Einstellungen für Kodierung und Fehlerbehandlung später zu ändern. Das Verhalten von Text-I/O-Operationen angesichts von Unicode-Problemen und -Mehrdeutigkeiten (z. B. Diakritika, Surrogates, ungültige Bytes in einer Kodierung) sollte dasselbe sein wie das der Unicode-Methoden encode()/decode(). UnicodeError kann ausgelöst werden.

Implementierungshinweis: Wir sollten in der Lage sein, einen Großteil der vom Modul codecs bereitgestellten Infrastruktur wiederzuverwenden. Wenn es nicht die genauen APIs bereitstellt, die wir benötigen, sollten wir es refaktorisieren, um eine Neuerfindung des Rads zu vermeiden.

Nicht-blockierendes I/O

Nicht-blockierendes I/O wird nur auf der Ebene des rohen I/O vollständig unterstützt. Wenn ein rohes Objekt im nicht-blockierenden Modus ist und eine Operation blockieren würde, geben `.read()` und `.readinto()` None zurück, während `.write()` 0 zurückgibt. Um ein Objekt in den nicht-blockierenden Modus zu versetzen, muss der Benutzer die Dateinummer extrahieren und dies manuell tun.

Auf den Ebenen des gepufferten I/O und des Text-I/O lösen Lese- oder Schreibfehler aufgrund einer nicht-blockierenden Bedingung eine IOError mit errno auf EAGAIN gesetzt aus.

Ursprünglich erwogen wir, das Verhalten des rohen I/O nach oben zu propagieren, aber viele Eckfälle und Probleme traten auf. Um diese Probleme zu lösen, wären erhebliche Änderungen an den Schichten für gepuffertes I/O und Text-I/O erforderlich gewesen. Was sollte beispielsweise `.flush()` bei einem gepufferten nicht-blockierenden Objekt tun? Wie würde der Benutzer das Objekt anweisen, "Schreibe so viel wie möglich aus deinem Puffer, aber blockiere nicht"? Ein nicht-blockierendes `.flush()`, das nicht unbedingt alle verfügbaren Daten leert, ist kontraintuitiv. Da nicht-blockierende und blockierende Objekte auf diesen Ebenen solch unterschiedliche Semantiken hätten, wurde vereinbart, die Bemühungen zur Kombination in einem einzigen Typ aufzugeben.

Die eingebaute Funktion open()

Die eingebaute Funktion open() wird durch den folgenden Pseudocode spezifiziert:

def open(filename, mode="r", buffering=None, *,
         encoding=None, errors=None, newline=None):
    assert isinstance(filename, (str, int))
    assert isinstance(mode, str)
    assert buffering is None or isinstance(buffering, int)
    assert encoding is None or isinstance(encoding, str)
    assert newline in (None, "", "\n", "\r", "\r\n")
    modes = set(mode)
    if modes - set("arwb+t") or len(mode) > len(modes):
        raise ValueError("invalid mode: %r" % mode)
    reading = "r" in modes
    writing = "w" in modes
    binary = "b" in modes
    appending = "a" in modes
    updating = "+" in modes
    text = "t" in modes or not binary
    if text and binary:
        raise ValueError("can't have text and binary mode at once")
    if reading + writing + appending > 1:
        raise ValueError("can't have read/write/append mode at once")
    if not (reading or writing or appending):
        raise ValueError("must have exactly one of read/write/append mode")
    if binary and encoding is not None:
        raise ValueError("binary modes doesn't take an encoding arg")
    if binary and errors is not None:
        raise ValueError("binary modes doesn't take an errors arg")
    if binary and newline is not None:
        raise ValueError("binary modes doesn't take a newline arg")
    # XXX Need to spec the signature for FileIO()
    raw = FileIO(filename, mode)
    line_buffering = (buffering == 1 or buffering is None and raw.isatty())
    if line_buffering or buffering is None:
        buffering = 8*1024  # International standard buffer size
        # XXX Try setting it to fstat().st_blksize
    if buffering < 0:
        raise ValueError("invalid buffering size")
    if buffering == 0:
        if binary:
            return raw
        raise ValueError("can't have unbuffered text I/O")
    if updating:
        buffer = BufferedRandom(raw, buffering)
    elif writing or appending:
        buffer = BufferedWriter(raw, buffering)
    else:
        assert reading
        buffer = BufferedReader(raw, buffering)
    if binary:
        return buffer
    assert text
    return TextIOWrapper(buffer, encoding, errors, newline, line_buffering)

Quelle: https://github.com/python/peps/blob/main/peps/pep-3116.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT