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

Python Enhancement Proposals

PEP 688 – Making the buffer protocol accessible in Python

Autor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Typ:
Standards Track
Thema:
Typisierung
Erstellt:
23. April 2022
Python-Version:
3.12
Post-History:
23. April 2022, 25. April 2022, 06. Oktober 2022, 26. Oktober 2022
Resolution:
07. März 2023

Inhaltsverzeichnis

Wichtig

Dieses PEP ist ein historisches Dokument. Die aktuellste, kanonische Dokumentation finden Sie unter Emulating buffer types.

×

Siehe PEP 1, um Änderungen vorzuschlagen.

Zusammenfassung

Dieses PEP schlägt eine Python-Level-API für das Buffer Protocol vor, das derzeit nur für C-Code zugänglich ist. Dies ermöglicht es Typenprüfern zu bewerten, ob Objekte das Protokoll implementieren.

Motivation

Die CPython C-API bietet einen vielseitigen Mechanismus für den Zugriff auf den zugrunde liegenden Speicher eines Objekts – das Buffer Protocol, das in PEP 3118 eingeführt wurde. Funktionen, die Binärdaten akzeptieren, sind normalerweise so geschrieben, dass sie jedes Objekt verarbeiten, das das Buffer Protocol implementiert. Zum Beispiel gibt es zum Zeitpunkt der Erstellung etwa 130 Funktionen in CPython, die den Argument Clinic Py_buffer Typ verwenden, der das Buffer Protocol akzeptiert.

Derzeit gibt es keine Möglichkeit für Python-Code, zu prüfen, ob ein Objekt das Buffer Protocol unterstützt. Darüber hinaus bietet das statische Typsystem keine Typannotation, um das Protokoll darzustellen. Dies ist ein häufiges Problem beim Schreiben von Typannotationen für Code, der generische Puffer akzeptiert.

Ebenso ist es für eine in Python geschriebene Klasse unmöglich, das Buffer Protocol zu unterstützen. Eine Pufferklasse in Python würde Benutzern die Möglichkeit geben, ein C-Pufferobjekt einfach zu umschließen oder das Verhalten einer API zu testen, die das Buffer Protocol verbraucht. Zugegeben, dies ist kein besonders häufiger Bedarf. Es gab jedoch eine CPython-Featureanfrage zur Unterstützung von Pufferklassen, die in Python geschrieben sind, die seit 2012 offen ist.

Begründung

Aktuelle Optionen

Es gibt zwei bekannte Workarounds für die Annotation von Puffertypen im Typsystem, aber keiner von beiden ist ausreichend.

Erstens ist der aktuelle Workaround für Puffertypen in typeshed ein Typalias, der bekannte Puffertypen in der Standardbibliothek auflistet, wie z. B. bytes, bytearray, memoryview und array.array. Dieser Ansatz funktioniert für die Standardbibliothek, aber er erstreckt sich nicht auf Drittanbieter-Puffertypen.

Zweitens besagt die Dokumentation von typing.ByteString derzeit

Dieser Typ repräsentiert die Typen bytes, bytearray und memoryview von Byte-Sequenzen.

Als Kurzschreibweise für diesen Typ kann bytes verwendet werden, um Argumente eines beliebigen der oben genannten Typen zu annotieren.

Obwohl dieser Satz seit 2015 in der Dokumentation steht, ist die Verwendung von bytes zur Einbeziehung dieser anderen Typen nicht in einem der Typen-PEP spezifiziert. Darüber hinaus hat dieser Mechanismus eine Reihe von Problemen. Er schließt nicht alle möglichen Puffertypen ein und macht den Typ bytes in Typannotationen mehrdeutig. Schließlich gibt es viele Operationen, die auf bytes-Objekten gültig sind, aber nicht auf memoryview-Objekten, und es ist durchaus möglich, dass eine Funktion bytes, aber nicht memoryview-Objekte akzeptiert. Ein mypy-Benutzer berichtet, dass diese Abkürzung erhebliche Probleme für das psycopg-Projekt verursacht hat.

Arten von Puffern

Das C-Buffer-Protokoll unterstützt viele Optionen, die sich auf Schritte, Kontinuität und Unterstützung für das Schreiben in den Puffer auswirken. Einige dieser Optionen wären im Typsystem nützlich. Zum Beispiel bietet typeshed derzeit separate Typaliase für beschreibbare und schreibgeschützte Puffer.

In den meisten Fällen können jedoch in C diese Optionen nicht direkt am Typobjekt abgefragt werden. Die einzige Möglichkeit, festzustellen, ob ein Objekt eine bestimmte Flagge unterstützt, besteht darin, tatsächlich nach dem Puffer zu fragen. Für einige Typen, wie z. B. memoryview, hängen die unterstützten Flags von der Instanz ab. Infolgedessen wäre es schwierig, die Unterstützung für diese Flags im Typsystem darzustellen.

Spezifikation

Python-Level Buffer Protocol

Wir schlagen vor, zwei Python-Level-Spezialmethoden hinzuzufügen: __buffer__ und __release_buffer__. Python-Klassen, die diese Methoden implementieren, können als Puffer aus C-Code verwendet werden. Umgekehrt erhalten in C implementierte Klassen, die das Buffer Protocol unterstützen, synthetisierte Methoden, die von Python-Code aus zugänglich sind.

Die Methode __buffer__ wird aufgerufen, um einen Puffer aus einem Python-Objekt zu erstellen, zum Beispiel durch den memoryview() Konstruktor. Sie entspricht dem bf_getbuffer C-Slot. Die Python-Signatur für diese Methode lautet def __buffer__(self, flags: int, /) -> memoryview: .... Die Methode muss ein memoryview-Objekt zurückgeben. Wenn der bf_getbuffer-Slot auf einer Python-Klasse mit einer __buffer__-Methode aufgerufen wird, extrahiert der Interpreter die zugrunde liegende Py_buffer aus dem von der Methode zurückgegebenen memoryview und gibt sie an den C-Aufrufer zurück. Wenn Python-Code die Methode __buffer__ für eine Instanz einer C-Klasse aufruft, die bf_getbuffer implementiert, wird der zurückgegebene Puffer in ein memoryview für die Verwendung durch Python-Code eingekapselt.

Die Methode __release_buffer__ sollte aufgerufen werden, wenn ein Aufrufer den von __buffer__ zurückgegebenen Puffer nicht mehr benötigt. Sie entspricht dem bf_releasebuffer C-Slot. Dies ist ein optionaler Teil des Buffer Protocols. Die Python-Signatur für diese Methode lautet def __release_buffer__(self, buffer: memoryview, /) -> None: .... Der freizugebende Puffer wird in ein memoryview eingekapselt. Wenn diese Methode über die Puffer-API von CPython aufgerufen wird (z. B. durch Aufruf von memoryview.release auf einem von __buffer__ zurückgegebenen memoryview), ist das übergebene memoryview dasselbe Objekt wie das von __buffer__ zurückgegebene. Es ist auch möglich, __release_buffer__ für eine C-Klasse aufzurufen, die bf_releasebuffer implementiert.

Wenn __release_buffer__ auf einem Objekt vorhanden ist, muss Python-Code, der __buffer__ direkt auf dem Objekt aufruft, __release_buffer__ auf demselben Objekt aufrufen, wenn er mit dem Puffer fertig ist. Andernfalls werden die vom Objekt verwendeten Ressourcen möglicherweise nicht freigegeben. Ebenso ist es ein Programmierfehler, __release_buffer__ ohne vorherigen Aufruf von __buffer__ aufzurufen oder ihn mehrmals für einen einzelnen Aufruf von __buffer__ aufzurufen. Für Objekte, die das C-Buffer-Protokoll implementieren, lösen Aufrufe von __release_buffer__, bei denen das Argument kein memoryview ist, das dasselbe Objekt umschließt, eine Ausnahme aus. Nach einem gültigen Aufruf von __release_buffer__ wird das memoryview ungültig (als ob seine release() Methode aufgerufen worden wäre), und alle nachfolgenden Aufrufe von __release_buffer__ mit demselben memoryview lösen eine Ausnahme aus. Der Interpreter stellt sicher, dass eine falsche Verwendung der Python-API keine Invarianten auf C-Ebene verletzt – z. B. keine Speicherverletzungen verursacht.

inspect.BufferFlags

Um Implementierungen von __buffer__ zu unterstützen, fügen wir inspect.BufferFlags hinzu, eine Unterklasse von enum.IntFlag. Dieses Enum enthält alle im C-Buffer-Protokoll definierten Flags. Zum Beispiel hat inspect.BufferFlags.SIMPLE denselben Wert wie die Konstante PyBUF_SIMPLE.

collections.abc.Buffer

Wir fügen eine neue abstrakte Basisklasse, collections.abc.Buffer, hinzu, die die Methode __buffer__ erfordert. Diese Klasse ist hauptsächlich für die Verwendung in Typannotationen gedacht

def need_buffer(b: Buffer) -> memoryview:
    return memoryview(b)

need_buffer(b"xy")  # ok
need_buffer("xy")  # rejected by static type checkers

Sie kann auch in isinstance und issubclass Überprüfungen verwendet werden

>>> from collections.abc import Buffer
>>> isinstance(b"xy", Buffer)
True
>>> issubclass(bytes, Buffer)
True
>>> issubclass(memoryview, Buffer)
True
>>> isinstance("xy", Buffer)
False
>>> issubclass(str, Buffer)
False

In den typeshed-Stubdateien sollte die Klasse als Protocol definiert werden, nach dem Vorbild anderer einfacher ABCs in collections.abc wie collections.abc.Iterable oder collections.abc.Sized.

Beispiel

Das Folgende ist ein Beispiel für eine Python-Klasse, die das Buffer Protocol implementiert

import contextlib
import inspect

class MyBuffer:
    def __init__(self, data: bytes):
        self.data = bytearray(data)
        self.view = None

    def __buffer__(self, flags: int) -> memoryview:
        if flags != inspect.BufferFlags.FULL_RO:
            raise TypeError("Only BufferFlags.FULL_RO supported")
        if self.view is not None:
            raise RuntimeError("Buffer already held")
        self.view = memoryview(self.data)
        return self.view

    def __release_buffer__(self, view: memoryview) -> None:
        assert self.view is view  # guaranteed to be true
        self.view.release()
        self.view = None

    def extend(self, b: bytes) -> None:
        if self.view is not None:
            raise RuntimeError("Cannot extend held buffer")
        self.data.extend(b)

buffer = MyBuffer(b"capybara")
with memoryview(buffer) as view:
    view[0] = ord("C")

    with contextlib.suppress(RuntimeError):
        buffer.extend(b"!")  # raises RuntimeError

buffer.extend(b"!")  # ok, buffer is no longer held

with memoryview(buffer) as view:
    assert view.tobytes() == b"Capybara!"

Äquivalent für ältere Python-Versionen

Neue Typen-Features werden normalerweise in ältere Python-Versionen im Paket typing_extensions zurückportiert. Da das Buffer Protocol derzeit nur in C zugänglich ist, kann dieses PEP nicht vollständig in einem reinen Python-Paket wie typing_extensions implementiert werden. Als temporärer Workaround wird eine abstrakte Basisklasse typing_extensions.Buffer für Python-Versionen bereitgestellt, für die collections.abc.Buffer nicht verfügbar ist.

Nach der Implementierung dieses PEP ist das Erben von collections.abc.Buffer nicht mehr notwendig, um anzuzeigen, dass ein Objekt das Buffer Protocol unterstützt. In älteren Python-Versionen ist es jedoch erforderlich, explizit von typing_extensions.Buffer zu erben, um Typenprüfern anzuzeigen, dass eine Klasse das Buffer Protocol unterstützt, da Objekte, die das Buffer Protocol unterstützen, keine __buffer__-Methode haben werden. Es wird erwartet, dass dies hauptsächlich in Stubdateien geschieht, da Pufferklassen zwangsläufig in C-Code implementiert werden, der keine inline definierten Typen haben kann. Für Laufzeitverwendungen kann die ABC.register API verwendet werden, um Pufferklassen bei typing_extensions.Buffer zu registrieren.

Keine spezielle Bedeutung für bytes

Der Sonderfall, der besagt, dass bytes als Kurzschreibweise für andere ByteString-Typen verwendet werden kann, wird aus der typing-Dokumentation entfernt. Da collections.abc.Buffer als Alternative verfügbar ist, wird es keinen guten Grund geben, bytes als Kurzschreibweise zuzulassen. Typenprüfer, die dieses Verhalten derzeit implementieren, sollten es deprecaten und schließlich entfernen.

Abwärtskompatibilität

__buffer__ und __release_buffer__ Attribute

Da die Laufzeitänderungen in diesem PEP nur neue Funktionalitäten hinzufügen, gibt es wenige Kompatibilitätsprobleme.

Code, der ein __buffer__- oder __release_buffer__-Attribut für andere Zwecke verwendet, kann jedoch betroffen sein. Obwohl alle Dunders technisch für die Sprache reserviert sind, ist es dennoch eine gute Praxis, sicherzustellen, dass ein neuer Dunder nicht zu viel bestehenden Code stört, insbesondere weit verbreitete Pakete. Eine Untersuchung von öffentlich zugänglichem Code ergab

  • PyPy unterstützt eine __buffer__-Methode mit kompatiblen Semantiken zu denen, die in diesem PEP vorgeschlagen werden. Ein PyPy-Kernentwickler drückte seine Unterstützung für dieses PEP aus.
  • pyzmq implementiert eine PyPy-kompatible __buffer__-Methode.
  • mpi4py definiert ein SupportsBuffer-Protokoll, das diesem PEPs collections.abc.Buffer entsprechen würde.
  • NumPy hatte früher ein undokumentiertes Verhalten, bei dem es auf ein __buffer__-Attribut (keine Methode) zugriff, um den Puffer eines Objekts zu erhalten. Dies wurde 2019 für NumPy 1.17 entfernt. Das Verhalten hätte zuletzt in NumPy 1.16 funktioniert, das nur Python 3.7 und älter unterstützte. Python 3.7 wird sein Lebensende erreicht haben, bis dieses PEP voraussichtlich implementiert wird.

Somit verbessert die Verwendung der __buffer__-Methode in diesem PEP die Interoperabilität mit PyPy und stört keine aktuellen Versionen großer Python-Pakete.

Kein öffentlich zugänglicher Code verwendet den Namen __release_buffer__.

Entfernung des Sonderfalls bytes

Separat hat die Empfehlung, den Sonderfall für bytes in Typenprüfern zu entfernen, eine Auswirkung auf die Kompatibilität mit ihren Benutzern. Ein Experiment mit mypy zeigt, dass mehrere große Open-Source-Projekte, die es zur Typenprüfung verwenden, neue Fehler sehen werden, wenn die bytes-Promotion entfernt wird. Viele dieser Fehler können durch die Verbesserung der Stubs in typeshed behoben werden, wie bereits für die builtins, binascii, pickle und re Module geschehen ist. Eine Überprüfung aller Verwendungen von bytes-Typen in typeshed ist im Gange. Insgesamt verbessert die Änderung die Typsicherheit und macht das Typsystem konsistenter, sodass wir glauben, dass die Migrationskosten es wert sind.

Wie man das lehrt

Wir werden Hinweise auf collections.abc.Buffer an geeigneten Stellen in der Dokumentation hinzufügen, wie z. B. auf typing.python.org und der mypy Cheat Sheet. Typenprüfer können zusätzliche Hinweise in ihren Fehlermeldungen bereitstellen. Wenn sie beispielsweise einen Pufferobjekt feststellen, das an eine Funktion übergeben wird, die nur für bytes annotiert ist, könnte die Fehlermeldung einen Hinweis enthalten, der die Verwendung von collections.abc.Buffer vorschlägt.

Referenzimplementierung

Eine Implementierung dieses PEP ist im Fork des Autors verfügbar.

Abgelehnte Ideen

types.Buffer

Eine frühere Version dieses PEP schlug vor, einen neuen types.Buffer-Typ mit einem in C implementierten __instancecheck__ hinzuzufügen, damit isinstance() Überprüfungen verwendet werden können, um zu prüfen, ob ein Typ das Buffer Protocol implementiert. Dies vermeidet die Komplexität, das vollständige Buffer Protocol für Python-Code freizulegen, erlaubt es dem Typsystem jedoch immer noch, auf das Buffer Protocol zu prüfen.

Dieser Ansatz lässt sich jedoch nicht gut mit dem Rest des Typsystems kombinieren, da types.Buffer ein nominaler Typ wäre, kein struktureller. Zum Beispiel gäbe es keine Möglichkeit, „ein Objekt darzustellen, das sowohl das Buffer Protocol als auch __len__ unterstützt“. Mit dem aktuellen Vorschlag ist __buffer__ wie jede andere Spezialmethode, sodass ein Protocol definiert werden kann, das sie mit einer anderen Methode kombiniert.

Allgemeiner ausgedrückt: Kein anderer Teil von Python funktioniert wie der vorgeschlagene types.Buffer. Der aktuelle Vorschlag ist konsistenter mit dem Rest der Sprache, bei der C-Level-Slots normalerweise entsprechende Python-Level-Spezialmethoden haben.

bytearray kompatibel mit bytes halten

Es wurde vorgeschlagen, den Sonderfall zu entfernen, dass memoryview immer mit bytes kompatibel ist, aber ihn für bytearray beizubehalten, da die beiden Typen sehr ähnliche Schnittstellen haben. Mehrere Funktionen der Standardbibliothek (z. B. re.compile, socket.getaddrinfo und die meisten Funktionen, die Pfad-ähnliche Argumente akzeptieren) akzeptieren jedoch bytes, aber nicht bytearray. In den meisten Codebasen ist bytearray auch kein sehr gebräuchlicher Typ. Wir ziehen es vor, dass Benutzer die akzeptierten Typen explizit angeben (oder Protocol aus PEP 544 verwenden, wenn nur ein bestimmter Satz von Methoden erforderlich ist). Dieser Aspekt des Vorschlags wurde speziell in der typing-sig Mailingliste diskutiert, ohne starke Einwände von der Typen-Community.

Unterscheidung zwischen veränderlichen und unveränderlichen Puffern

Die am häufigsten verwendete Unterscheidung zwischen Puffertypen ist, ob der Puffer veränderlich ist oder nicht. Einige Funktionen akzeptieren nur veränderliche Puffer (z. B. bytearray, einige memoryview-Objekte), andere akzeptieren alle Puffer.

Eine frühere Version dieses PEP schlug vor, das Vorhandensein des bf_releasebuffer-Slots zu verwenden, um zu bestimmen, ob ein Puffertyp veränderlich ist. Diese Regel gilt für die meisten Puffer-Typen der Standardbibliothek, aber die Beziehung zwischen Veränderlichkeit und dem Vorhandensein dieses Slots ist nicht absolut. Zum Beispiel sind numpy-Arrays veränderlich, haben aber diesen Slot nicht.

Das aktuelle Buffer Protocol bietet keine Möglichkeit, zuverlässig zu bestimmen, ob ein Puffertyp einen veränderlichen oder unveränderlichen Puffer darstellt. Daher fügt dieses PEP keine Unterstützung für dieses Typsystem hinzu. Die Frage kann in Zukunft wieder aufgegriffen werden, wenn das Buffer Protocol erweitert wird, um statische Introspektionsunterstützung bereitzustellen. Ein Entwurf für einen solchen Mechanismus existiert.

Danksagungen

Viele Leute haben nützliches Feedback zu Entwürfen dieses PEP gegeben. Petr Viktorin war besonders hilfreich bei der Verbesserung meines Verständnisses der Feinheiten des Buffer Protocols.


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

Zuletzt geändert: 05.03.2025 16:28:34 GMT