PEP 574 – Pickle-Protokoll 5 mit Out-of-Band-Daten
- Autor:
- Antoine Pitrou <solipsis at pitrou.net>
- BDFL-Delegate:
- Alyssa Coghlan
- Status:
- Final
- Typ:
- Standards Track
- Erstellt:
- 23. März 2018
- Python-Version:
- 3.8
- Post-History:
- 28. März 2018, 30. April 2019
- Resolution:
- Python-Dev Nachricht
Zusammenfassung
Dieses PEP schlägt die Standardisierung einer neuen Version des Pickle-Protokolls und begleitender APIs vor, um diese vollständig zu nutzen.
- Eine neue Pickle-Protokollversion (5), um die zusätzlichen Metadaten für Out-of-Band-Datenpuffer zu erfassen.
- Ein neuer
PickleBuffer-Typ für__reduce_ex__-Implementierungen, um Out-of-Band-Datenpuffer zurückzugeben. - Ein neuer
buffer_callback-Parameter beim Pickling zur Handhabung von Out-of-Band-Datenpuffern. - Ein neuer
buffers-Parameter beim Unpickling zur Bereitstellung von Out-of-Band-Datenpuffern.
Das PEP garantiert unverändertes Verhalten für alle, die die neuen APIs nicht verwenden.
Begründung
Das Pickle-Protokoll wurde ursprünglich 1995 für die Persistenz beliebiger Python-Objekte auf der Festplatte entwickelt. Die Leistung eines Speichermediums aus den 1990er Jahren machte es wahrscheinlich irrelevant, sich auf Leistungsmetriken wie die Nutzung von RAM-Bandbreite beim Kopieren temporärer Daten vor dem Schreiben auf die Festplatte zu konzentrieren.
Heutzutage findet das Pickle-Protokoll eine wachsende Anwendung in Bereichen, in denen die meisten Daten niemals auf der Festplatte gespeichert werden (oder, wenn doch, ein portables Format anstelle eines Python-spezifischen verwendet wird). Stattdessen wird Pickle verwendet, um Daten und Befehle von einem Prozess zu einem anderen zu übertragen, entweder auf demselben oder auf mehreren Rechnern. Diese Anwendungen befassen sich manchmal mit sehr großen Daten (wie Numpy-Arrays oder Pandas-DataFrames), die übertragen werden müssen. Für diese Anwendungen ist Pickle derzeit verschwenderisch, da es unnötige Speicherkopien der serialisierten Daten verursacht.
Tatsächlich verwendet das Standardmodul multiprocessing Pickle für die Serialisierung und leidet daher ebenfalls unter diesem Problem, wenn große Daten an einen anderen Prozess gesendet werden.
Drittanbieter-Python-Bibliotheken wie Dask [1], PyArrow [4] und IPyParallel [3] haben begonnen, alternative Serialisierungsschemata zu implementieren, mit dem ausdrücklichen Ziel, Kopien bei großen Daten zu vermeiden. Die Implementierung eines neuen Serialisierungsschemas ist schwierig und führt oft zu einer geringeren Allgemeingültigkeit (da viele Python-Objekte Pickle unterstützen, aber nicht das neue Serialisierungsschema). Ein Rückgriff auf Pickle für nicht unterstützte Typen ist eine Option, aber dann erhalten Sie die unnötigen Speicherkopien zurück, die Sie ursprünglich vermeiden wollten. Zum Beispiel kann dask Speicherkopien für Numpy-Arrays und integrierte Container davon (wie Listen oder Wörterbücher, die Numpy-Arrays enthalten) vermeiden, aber wenn ein großes Numpy-Array ein Attribut eines benutzerdefinierten Objekts ist, serialisiert dask das benutzerdefinierte Objekt als Pickle-Stream, was zu Speicherkopien führt.
Das gemeinsame Merkmal dieser Bemühungen zur Serialisierung durch Drittanbieter ist die Erzeugung eines Streams von Objektmetadaten (der pickle-ähnliche Informationen über die serialisierten Objekte enthält) und eines separaten Streams von Null-Kopie-Pufferobjekten für die Payloads großer Objekte. Beachten Sie, dass in diesem Schema kleine Objekte wie Integers usw. zusammen mit dem Metadaten-Stream übertragen werden können. Verfeinerungen können die opportunistische Komprimierung großer Daten je nach Typ und Layout umfassen, wie dask es tut.
Dieses PEP zielt darauf ab, pickle so nutzbar zu machen, dass große Daten als separater Stream von Null-Kopie-Puffern behandelt werden, was es der Anwendung ermöglicht, diese Puffer optimal zu handhaben.
Beispiel
Um das Beispiel einfach zu halten und das Wissen über Drittanbieterbibliotheken nicht vorauszusetzen, konzentrieren wir uns hier auf ein bytearray-Objekt (aber das Problem ist konzeptionell dasselbe bei ausgefeilteren Objekten wie Numpy-Arrays). Wie die meisten Objekte wird das bytearray-Objekt vom Pickle-Modul nicht sofort verstanden und muss daher sein Zerlegungsschema angeben.
Hier ist, wie sich ein bytearray-Objekt derzeit für das Pickling zerlegt:
>>> b.__reduce_ex__(4)
(<class 'bytearray'>, (b'abc',), None)
Dies liegt daran, dass die bytearray.__reduce_ex__-Implementierung moralisch wie folgt gelesen wird:
class bytearray:
def __reduce_ex__(self, protocol):
if protocol == 4:
return type(self), bytes(self), None
# Legacy code for earlier protocols omitted
Im Gegenzug erzeugt dies den folgenden Pickle-Code:
>>> pickletools.dis(pickletools.optimize(pickle.dumps(b, protocol=4)))
0: \x80 PROTO 4
2: \x95 FRAME 30
11: \x8c SHORT_BINUNICODE 'builtins'
21: \x8c SHORT_BINUNICODE 'bytearray'
32: \x93 STACK_GLOBAL
33: C SHORT_BINBYTES b'abc'
38: \x85 TUPLE1
39: R REDUCE
40: . STOP
(Der Aufruf von pickletools.optimize oben dient nur dazu, den Pickle-Stream lesbarer zu machen, indem die MEMOIZE-Opcodes entfernt werden.)
Wir können mehrere Dinge über die Payload des bytearray (die Bytesequenz b'abc') bemerken:
bytearray.__reduce_ex__erzeugt eine erste Kopie, indem es ein neues Bytes-Objekt aus den Daten des bytearrays instanziiert.pickle.dumpserzeugt eine zweite Kopie, wenn der Inhalt dieses Bytes-Objekts in den Pickle-Stream eingefügt wird, nach dem SHORT_BINBYTES-Opcode.- Darüber hinaus wird beim Deserialisieren des Pickle-Streams ein temporäres Bytes-Objekt erstellt, wenn der SHORT_BINBYTES-Opcode angetroffen wird (was eine Datenkopie induziert).
Was wir wirklich wollen, ist so etwas wie das Folgende:
bytearray.__reduce_ex__erzeugt eine *Ansicht* der Daten des bytearrays.pickle.dumpsversucht nicht, diese Daten in den Pickle-Stream zu kopieren, sondern übergibt die Pufferansicht an seinen Aufrufer (der über die effizienteste Handhabung dieses Puffers entscheiden kann).- Beim Deserialisieren nimmt
pickle.loadsden Pickle-Stream und die Pufferansicht separat entgegen und übergibt die Pufferansicht direkt an den bytearray-Konstruktor.
Wir sehen, dass mehrere Bedingungen für das Gelingen des Obigen erforderlich sind:
__reduce__oder__reduce_ex__muss *etwas* zurückgeben können, das eine serialisierbare Null-Kopie-Pufferansicht anzeigt.- Das Pickle-Protokoll muss in der Lage sein, Referenzen auf solche Pufferansichten darzustellen und den Unpickler anzuweisen, dass er den tatsächlichen Puffer möglicherweise out-of-band abrufen muss.
- Die
pickle.Pickler-API muss ihrem Aufrufer eine Möglichkeit bieten, solche Pufferansichten während der Serialisierung zu empfangen. - Die
pickle.Unpickler-API muss es ihrem Aufrufer ebenfalls ermöglichen, die für die Deserialisierung erforderlichen Pufferansichten bereitzustellen. - Aus Kompatibilitätsgründen muss das Pickle-Protokoll auch direkte Serialisierungen solcher Pufferansichten enthalten können, so dass aktuelle Verwendungen der
pickle-API nicht geändert werden müssen, wenn sie sich nicht um Speicherkopien kümmern.
Producer-API
Wir führen einen neuen Typ pickle.PickleBuffer ein, der aus jedem Puffer unterstützenden Objekt instanziiert werden kann und speziell dafür vorgesehen ist, von __reduce__-Implementierungen zurückgegeben zu werden.
class bytearray:
def __reduce_ex__(self, protocol):
if protocol >= 5:
return type(self), (PickleBuffer(self),), None
# Legacy code for earlier protocols omitted
PickleBuffer ist ein einfacher Wrapper, der nicht über alle Memoryview-Semantiken und -Funktionalitäten verfügt, aber vom pickle-Modul erkannt wird, wenn Protokoll 5 oder höher aktiviert ist. Es ist ein Fehler, zu versuchen, einen PickleBuffer mit Pickle-Protokollversion 4 oder früher zu serialisieren.
Nur die rohen *Daten* des PickleBuffer werden vom pickle-Modul berücksichtigt. Alle typspezifischen *Metadaten* (wie Formen oder Datentypen) müssen separat von der __reduce__-Implementierung des Typs zurückgegeben werden, wie es bereits der Fall ist.
PickleBuffer-Objekte
Die Klasse PickleBuffer unterstützt eine sehr einfache Python-API. Ihr Konstruktor nimmt ein einziges PEP 3118-kompatibles Objekt entgegen. PickleBuffer-Objekte selbst unterstützen das Pufferprotokoll, sodass Verbraucher memoryview(...) darauf aufrufen können, um zusätzliche Informationen über den zugrunde liegenden Puffer zu erhalten (wie z. B. den ursprünglichen Typ, die Form usw.). Darüber hinaus verfügen PickleBuffer-Objekte über die folgenden Methoden:
raw()
Gibt eine Memoryview der rohen Speicherbyte zurück, die dem PickleBuffer zugrunde liegen, und löscht alle Informationen über Form, Schrittweiten und Format. Dies ist erforderlich, um Fortran-kontinuierliche Puffer in der reinen Python-Pickle-Implementierung korrekt zu handhaben.
release()
Gibt den zugrunde liegenden Puffer des PickleBuffer frei und macht ihn unbrauchbar.
Auf der C-Seite wird eine einfache API bereitgestellt, um PickleBuffer-Objekte zu erstellen und zu inspizieren.
PyObject *PyPickleBuffer_FromObject(PyObject *obj)
Erstellt einPickleBuffer-Objekt, das eine Ansicht über das PEP 3118-kompatible *obj* enthält.
PyPickleBuffer_Check(PyObject *obj)
Gibt zurück, ob *obj* eine Instanz vonPickleBufferist.
const Py_buffer *PyPickleBuffer_GetBuffer(PyObject *picklebuf)
Gibt einen Zeiger auf den internenPy_bufferzurück, der von derPickleBuffer-Instanz gehalten wird. Eine Ausnahme wird ausgelöst, wenn der Puffer freigegeben ist.
int PyPickleBuffer_Release(PyObject *picklebuf)
Gibt den zugrunde liegenden Puffer derPickleBuffer-Instanz frei.
Buffer-Anforderungen
PickleBuffer kann jede Art von Puffer wrappen, einschließlich nicht zusammenhängender Puffer. Es ist jedoch erforderlich, dass __reduce__ nur einen zusammenhängenden PickleBuffer zurückgibt (*zusammenhängend* ist hier im Sinne von PEP 3118 zu verstehen: entweder C-Ordnung oder Fortran-Ordnung). Nicht zusammenhängende Puffer lösen beim Pickling einen Fehler aus.
Diese Einschränkung ist hauptsächlich ein Problem der Implementierungsvereinfachung für das pickle-Modul, aber auch für andere Verbraucher von Out-of-Band-Puffern. Die einfachste Lösung auf Anbieterseite ist, eine zusammenhängende Kopie eines nicht zusammenhängenden Puffers zurückzugeben; ein ausgefeilter Anbieter kann sich jedoch stattdessen entscheiden, eine Sequenz zusammenhängender Teilpuffer zurückzugeben.
Consumer-API
pickle.Pickler.__init__ und pickle.dumps werden um einen zusätzlichen buffer_callback-Parameter erweitert.
class Pickler:
def __init__(self, file, protocol=None, ..., buffer_callback=None):
"""
If *buffer_callback* is None (the default), buffer views are
serialized into *file* as part of the pickle stream.
If *buffer_callback* is not None, then it can be called any number
of times with a buffer view. If the callback returns a false value
(such as None), the given buffer is out-of-band; otherwise the
buffer is serialized in-band, i.e. inside the pickle stream.
The callback should arrange to store or transmit out-of-band buffers
without changing their order.
It is an error if *buffer_callback* is not None and *protocol* is
None or smaller than 5.
"""
def pickle.dumps(obj, protocol=None, *, ..., buffer_callback=None):
"""
See above for *buffer_callback*.
"""
pickle.Unpickler.__init__ und pickle.loads werden um einen zusätzlichen buffers-Parameter erweitert.
class Unpickler:
def __init__(file, *, ..., buffers=None):
"""
If *buffers* is not None, it should be an iterable of buffer-enabled
objects that is consumed each time the pickle stream references
an out-of-band buffer view. Such buffers have been given in order
to the *buffer_callback* of a Pickler object.
If *buffers* is None (the default), then the buffers are taken
from the pickle stream, assuming they are serialized there.
It is an error for *buffers* to be None if the pickle stream
was produced with a non-None *buffer_callback*.
"""
def pickle.loads(data, *, ..., buffers=None):
"""
See above for *buffers*.
"""
Protokolländerungen
Drei neue Opcodes werden eingeführt:
BYTEARRAY8erstellt ein bytearray aus den folgenden Daten im Pickle-Stream und legt es auf den Stack (genau wieBINBYTES8für Bytes-Objekte);NEXT_BUFFERruft einen Puffer aus dembuffers-Iterable ab und legt ihn auf den Stack.READONLY_BUFFERerstellt eine schreibgeschützte Ansicht vom oberen Element des Stacks.
Wenn beim Pickling ein PickleBuffer angetroffen wird, kann dieser Puffer je nach folgenden Bedingungen als In-Band oder Out-of-Band betrachtet werden:
- Wenn kein
buffer_callbackangegeben ist, ist der Puffer In-Band; - Wenn ein
buffer_callbackangegeben ist, wird dieser mit dem Puffer aufgerufen. Wenn der Callback einen wahren Wert zurückgibt, ist der Puffer In-Band; wenn der Callback einen falschen Wert zurückgibt, ist der Puffer Out-of-Band.
Ein In-Band-Puffer wird wie folgt serialisiert:
- Wenn der Puffer beschreibbar ist, wird er in den Pickle-Stream serialisiert, als wäre er ein
bytearray-Objekt. - Wenn der Puffer schreibgeschützt ist, wird er in den Pickle-Stream serialisiert, als wäre er ein
bytes-Objekt.
Ein Out-of-Band-Puffer wird wie folgt serialisiert:
- Wenn der Puffer beschreibbar ist, wird ein
NEXT_BUFFER-Opcode an den Pickle-Stream angehängt. - Wenn der Puffer schreibgeschützt ist, wird ein
NEXT_BUFFER-Opcode an den Pickle-Stream angehängt, gefolgt von einemREADONLY_BUFFER-Opcode.
Die Unterscheidung zwischen schreibgeschützten und beschreibbaren Puffern wird unten erläutert (siehe "Änderbarkeit").
Nebeneffekte
Verbesserte In-Band-Leistung
Selbst In-Band-Pickling kann durch die Rückgabe einer PickleBuffer-Instanz von __reduce_ex__ verbessert werden, da auf dem Serialisierungspfad eine Kopie vermieden wird [10] [12].
Hinweise
Änderbarkeit
PEP 3118-Puffer können schreibgeschützt oder beschreibbar sein. Einige Objekte, wie Numpy-Arrays, müssen für eine vollständige Funktionalität durch einen veränderbaren Puffer unterstützt werden. Pickle-Verbraucher, die die Argumente buffer_callback und buffers verwenden, müssen vorsichtig sein, um veränderbare Puffer neu zu erstellen. Bei I/O bedeutet dies die Verwendung von Puffer-Passing-API-Varianten wie readinto (die oft auch aus Leistungsgründen vorzuziehen sind).
Datenaustausch
Wenn Sie ein Objekt im selben Prozess picklen und dann unpicklen, wobei Out-of-Band-Pufferansichten übergeben werden, kann das unpickelte Objekt durch denselben Puffer wie das ursprüngliche gepickelte Objekt gesichert sein.
Zum Beispiel könnte es sinnvoll sein, die Reduzierung eines Numpy-Arrays wie folgt zu implementieren (wichtige Metadaten wie Formen sind der Einfachheit halber weggelassen):
class ndarray:
def __reduce_ex__(self, protocol):
if protocol == 5:
return numpy.frombuffer, (PickleBuffer(self), self.dtype)
# Legacy code for earlier protocols omitted
Das einfache Übergeben des PickleBuffer von dumps an loads erzeugt dann ein neues Numpy-Array, das denselben zugrunde liegenden Speicher wie das ursprüngliche Numpy-Objekt teilt (und es übrigens am Leben erhält).
>>> import numpy as np
>>> a = np.zeros(10)
>>> a[0]
0.0
>>> buffers = []
>>> data = pickle.dumps(a, protocol=5, buffer_callback=buffers.append)
>>> b = pickle.loads(data, buffers=buffers)
>>> b[0] = 42
>>> a[0]
42.0
Dies geschieht nicht mit der traditionellen pickle-API (d. h. ohne Übergabe der Parameter buffers und buffer_callback), da die Pufferansicht dann mit einer Kopie innerhalb des Pickle-Streams serialisiert wird.
Abgelehnte Alternativen
Verwendung der bestehenden persistent load-Schnittstelle
Die pickle-Persistenzschnittstelle ist eine Möglichkeit, Referenzen auf designierte Objekte im Pickle-Stream zu speichern und deren eigentliche Serialisierung out-of-band zu handhaben. Zum Beispiel könnte man für die Null-Kopie-Serialisierung von bytearrays Folgendes in Betracht ziehen:
class MyPickle(pickle.Pickler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.buffers = []
def persistent_id(self, obj):
if type(obj) is not bytearray:
return None
else:
index = len(self.buffers)
self.buffers.append(obj)
return ('bytearray', index)
class MyUnpickle(pickle.Unpickler):
def __init__(self, *args, buffers, **kwargs):
super().__init__(*args, **kwargs)
self.buffers = buffers
def persistent_load(self, pid):
type_tag, index = pid
if type_tag == 'bytearray':
return self.buffers[index]
else:
assert 0 # unexpected type
Dieser Mechanismus hat zwei Nachteile:
- Jeder
pickle-Konsument muss benutzerdefiniertePickler- undUnpickler-Unterklassen mit speziellem Code für jeden interessierenden Typ neu implementieren. Im Wesentlichen implementieren N Pickle-Konsumenten jeweils benutzerdefinierten Code für M Produzenten. Dies ist schwierig (insbesondere für ausgefeilte Typen wie Numpy-Arrays) und schlecht skalierbar. - Jedes vom Pickle-Modul angetroffene Objekt (auch einfache integrierte Objekte wie Integers und Strings) löst einen Aufruf der
persistent_id()-Methode des Benutzers aus, was zu einem möglichen Leistungsabfall im Vergleich zum Nominal führt.(Das
cPickle-Modul in Python 2 unterstützte einen undokumentierteninst_persistent_id()-Hook, der nur auf Nicht-Built-in-Typen aufgerufen wurde; er wurde 1997 hinzugefügt, um das Leistungsproblem des Aufrufs vonpersistent_idzu mildern, vermutlich auf Anfrage von ZODB).
Übergabe einer Sequenz von Puffern in buffer_callback
Durch die Übergabe einer Sequenz von Puffern anstelle eines einzelnen Puffers würden wir potenziell Funktionsaufruf-Overhead einsparen, falls während der Serialisierung eine große Anzahl von Puffern erzeugt wird. Dies würde zusätzliche Unterstützung im Pickler erfordern, um Puffer vor dem Aufruf des Callbacks zu speichern. Es würde jedoch auch verhindern, dass der Puffer-Callback einen booleschen Wert zurückgibt, um anzuzeigen, ob ein Puffer In-Band oder Out-of-Band serialisiert werden soll.
Wir gehen davon aus, dass die Serialisierung einer großen Anzahl von Puffern ein unwahrscheinlicher Fall ist, und haben beschlossen, dem Puffer-Callback einen einzelnen Puffer zu übergeben.
Erlaubt das Serialisieren eines PickleBuffer in Protokoll 4 und früher
Wenn wir erlauben würden, einen PickleBuffer in Protokollen 4 und früher zu serialisieren, würde dies tatsächlich eine zusätzliche Speicher kopieren, wenn der Puffer veränderbar ist. Tatsächlich würde ein veränderbarer PickleBuffer in diesen Protokollen als bytearray-Objekt serialisiert werden (das ist eine erste Kopie), und die Serialisierung des bytearray-Objekts würde bytearray.__reduce_ex__ aufrufen, was ein bytes-Objekt zurückgibt (das ist eine zweite Kopie).
Um Implementierer von __reduce__ daran zu hindern, unfreiwillige Leistungsregressionen einzuführen, haben wir beschlossen, PickleBuffer abzulehnen, wenn das Protokoll kleiner als 5 ist. Dies zwingt die Implementierer, auf __reduce_ex__ umzusteigen und protokollabhängige Serialisierung zu implementieren, wobei der beste Pfad für jedes Protokoll genutzt wird (oder zumindest Protokoll 5 und höher von Protokollen 4 und darunter getrennt behandelt wird).
Implementierung
Das PEP wurde ursprünglich in der GitHub-Fork des Autors implementiert [6]. Es wurde später in Python 3.8 integriert [7].
Ein Backport für Python 3.6 und 3.7 kann von PyPI heruntergeladen werden [8].
Die Unterstützung für das Pickle-Protokoll 5 und Out-of-Band-Puffer wurde in Numpy hinzugefügt [11].
Die Unterstützung für das Pickle-Protokoll 5 und Out-of-Band-Puffer wurde zu den Apache Arrow Python-Bindings hinzugefügt [9].
Danksagungen
Dank der folgenden Personen für frühes Feedback: Alyssa Coghlan, Olivier Grisel, Stefan Krah, MinRK, Matt Rocklin, Eric Snow.
Dank an Pierre Glaser und Olivier Grisel für Experimente mit der Implementierung.
Referenzen
Urheberrecht
Dieses Dokument wurde in den öffentlichen Bereich gestellt.
Quelle: https://github.com/python/peps/blob/main/peps/pep-0574.rst
Zuletzt geändert: 2025-02-01 08:59:27 GMT