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
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 Typenbytes,bytearrayundmemoryviewvon Byte-Sequenzen.Als Kurzschreibweise für diesen Typ kann
bytesverwendet 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 PEPscollections.abc.Bufferentsprechen 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.
Urheberrecht
Dieses Dokument wird in die Public Domain oder unter die CC0-1.0-Universal-Lizenz gestellt, je nachdem, welche Lizenz permissiver ist.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0688.rst
Zuletzt geändert: 05.03.2025 16:28:34 GMT