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

Python Enhancement Proposals

PEP 3154 – Pickle-Protokoll Version 4

Autor:
Antoine Pitrou <solipsis at pitrou.net>
Status:
Final
Typ:
Standards Track
Erstellt:
11. August 2011
Python-Version:
3.4
Post-History:
12. August 2011
Resolution:
Python-Dev Nachricht

Inhaltsverzeichnis

Zusammenfassung

Daten, die mit dem Pickle-Modul serialisiert werden, müssen zwischen Python-Versionen portierbar sein. Sie sollten auch die neuesten Sprachfunktionen sowie implementierungsspezifische Funktionen unterstützen. Aus diesem Grund kennt das Pickle-Modul mehrere Protokolle (derzeit von 0 bis 3 nummeriert), die jeweils in einer anderen Python-Version erschienen sind. Die Verwendung einer Protokollversion mit niedriger Nummer ermöglicht den Datenaustausch mit alten Python-Versionen, während die Verwendung einer Protokollversion mit hoher Nummer den Zugriff auf neuere Funktionen und manchmal eine effizientere Ressourcennutzung ermöglicht (sowohl die CPU-Zeit für die (De-)Serialisierung als auch die Festplattengröße / Netzwerkbandbreite für die Datenübertragung).

Begründung

Das aktuellste Protokoll, zufällig Protokoll 3 genannt, erschien mit Python 3.0 und unterstützt die neuen inkompatiblen Funktionen in der Sprache (hauptsächlich standardmäßig Unicode-Zeichenketten und das neue Bytes-Objekt). Die Gelegenheit wurde damals nicht genutzt, um das Protokoll auf andere Weise zu verbessern.

Diese PEP ist ein Versuch, eine Reihe inkrementeller Verbesserungen in einer neuen Pickle-Protokollversion zu fördern. Das PEP-Verfahren wird verwendet, um so viele Verbesserungen wie möglich zu sammeln, da die Einführung eines neuen Pickle-Protokolls eine seltene Angelegenheit sein sollte.

Vorgeschlagene Änderungen

Framing

Traditionell kann beim Entpickeln eines Objekts aus einem Stream (durch Aufruf von `load() statt `loads()) viele kleine `read()-Aufrufe auf dem dateiähnlichen Objekt ausgegeben werden, was sich potenziell erheblich auf die Leistung auswirken kann.

Protokoll 4 zeichnet sich dagegen durch binäres Framing aus. Die allgemeine Struktur eines Pickles ist somit die folgende:

+------+------+
| 0x80 | 0x04 |              protocol header (2 bytes)
+------+------+
|  OP  |                     FRAME opcode (1 byte)
+------+------+-----------+
| MM MM MM MM MM MM MM MM |  frame size (8 bytes, little-endian)
+------+------------------+
| .... |                     first frame contents (M bytes)
+------+
|  OP  |                     FRAME opcode (1 byte)
+------+------+-----------+
| NN NN NN NN NN NN NN NN |  frame size (8 bytes, little-endian)
+------+------------------+
| .... |                     second frame contents (N bytes)
+------+
  etc.

Um die Implementierung einfach zu halten, darf ein Pickle-Opcode keine Frame-Grenzen überschreiten. Der Pickler stellt sicher, keine solchen Pickles zu erzeugen, und der Unpickler lehnt sie ab. Außerdem gibt es keine Markierung für den „letzten Frame“. Der letzte Frame ist einfach derjenige, der mit einem STOP-Opcode endet.

Eine gut geschriebene C-Implementierung benötigt keine zusätzlichen Speicher kopien für die Framing-Schicht und erhält die allgemeine Effizienz der (Ent-)Serialisierung.

Hinweis

Wie der Pickler den Pickle-Stream in Frames aufteilt, ist ein Implementierungsdetail. Zum Beispiel ist das „Schließen“ eines Frames, sobald er etwa 64 KiB erreicht, eine vernünftige Wahl sowohl für die Leistung als auch für den Overhead der Pickle-Größe.

Binäre Kodierung für alle Opcodes

Der GLOBAL-Opcode, der in Protokoll 3 immer noch verwendet wird, nutzt den sogenannten „Textmodus“ des Pickle-Protokolls, der das Suchen nach Zeilenumbrüchen im Pickle-Stream beinhaltet. Dies erschwert auch die Implementierung von binärem Framing.

Protokoll 4 verbietet die Verwendung des GLOBAL-Opcodes und ersetzt ihn durch STACK_GLOBAL, einen neuen Opcode, der seinen Operanden vom Stack nimmt.

Serialisierung von mehr „nachschlagefähigen“ Objekten

Standardmäßig kann Pickle nur Modul-globale Funktionen und Klassen serialisieren. Die Unterstützung anderer Objekttypen, wie z. B. unbound methods [4], ist eine häufige Anforderung. Tatsächlich wird von Drittanbietern die Unterstützung für einige davon, wie z. B. bound methods, im multiprocessing-Modul implementiert [5].

Das Attribut `__qualname__` aus PEP 3155 ermöglicht das Nachschlagen vieler weiterer Objekte anhand ihres Namens. Wenn der STACK_GLOBAL-Opcode punktgetrennte Namen akzeptiert, kann die Standard-Pickle-Implementierung all diese Objekttypen unterstützen.

64-Bit-Opcodes für große Objekte

Aktuelle Protokollversionen exportieren Objektgrößen für verschiedene eingebaute Typen (str, bytes) als 32-Bit-Integer. Dies verbietet die Serialisierung großer Daten [1]. Neue Opcodes sind erforderlich, um sehr große Bytes- und String-Objekte zu unterstützen.

Native Opcodes für Mengen (sets) und unveränderliche Mengen (frozensets)

Viele gängige eingebaute Typen (wie str, bytes, dict, list, tuple) haben dedizierte Opcodes, um den Ressourcenverbrauch beim Serialisieren und Deserialisieren zu verbessern; Mengen (sets) und unveränderliche Mengen (frozensets) jedoch nicht. Das Hinzufügen solcher Opcodes wäre eine offensichtliche Verbesserung. Außerdem könnte die dedizierte Unterstützung für Mengen dazu beitragen, die derzeitige Unmöglichkeit, sich selbst referenzierende Mengen zu pickeln, zu beseitigen [2].

Aufruf von `__new__` mit Schlüsselwortargumenten

Derzeit können Klassen, deren `__new__` die Verwendung von nur-Schlüsselwortargumenten erzwingt, nicht gepickelt (oder besser gesagt, nicht entpickelt) werden [3]. Sowohl eine neue spezielle Methode (`__getnewargs_ex__`) als auch ein neuer Opcode (NEWOBJ_EX) sind erforderlich. Die Methode `__getnewargs_ex__`, falls vorhanden, muss ein Tupel aus zwei Elementen zurückgeben (`(args, kwargs)`), wobei das erste Element das Tupel der Positionsargumente und das zweite Element das Dictionary der Schlüsselwortargumente für die `__new__`-Methode der Klasse ist.

Bessere Zeichenkodierung

Kurze String-Objekte haben derzeit ihre Länge als 4-Byte-Integer kodiert, was verschwenderisch ist. Ein spezifischer Opcode mit einer 1-Byte-Länge würde viele Pickles kleiner machen.

Kleinere Memoization

Die PUT-Opcodes erfordern alle einen expliziten Index, um im Memo-Dictionary zu wählen, in welchem Eintrag das oberste Element des Stacks memoisiert wird. In der Praxis werden diese Nummern jedoch sequenziell zugewiesen. Ein neuer Opcode, MEMOIZE, speichert stattdessen das oberste Element des Stacks unter dem Index, der der aktuellen Größe des Memo-Dictionaries entspricht. Dies ermöglicht kürzere Pickles, da PUT-Opcodes für alle nicht-atomaren Datentypen ausgegeben werden.

Zusammenfassung der neuen Opcodes

Diese spiegeln den Stand der vorgeschlagenen Implementierung wider (hauptsächlich dank der Arbeit von Alexandre Vassalotti)

  • FRAME: Einführung eines neuen Frames (gefolgt von der 8-Byte-Frame-Größe und dem Frame-Inhalt).
  • SHORT_BINUNICODE: Push eines UTF8-kodierten String-Objekts mit einem Ein-Byte-Größenpräfix (daher weniger als 256 Bytes lang).
  • BINUNICODE8: Push eines UTF8-kodierten String-Objekts mit einem Acht-Byte-Größenpräfix (für Strings, die länger als 2**32 Bytes sind und daher nicht mit `BINUNICODE` serialisiert werden können).
  • BINBYTES8: Push eines Bytes-Objekts mit einem Acht-Byte-Größenpräfix (für Bytes-Objekte, die länger als 2**32 Bytes sind und daher nicht mit `BINBYTES` serialisiert werden können).
  • EMPTY_SET: Push eines neuen leeren Set-Objekts auf den Stack.
  • ADDITEMS: Fügt die obersten Stack-Elemente zum Set hinzu (wird mit `EMPTY_SET` verwendet).
  • FROZENSET: Erstellt ein frozenset-Objekt aus den obersten Stack-Elementen und pusht es auf den Stack.
  • NEWOBJ_EX: Nimmt die drei obersten Stack-Elemente `cls`, `args` und `kwargs` und pusht das Ergebnis des Aufrufs von `cls.__new__(*args, **kwargs)`.
  • STACK_GLOBAL: Nimmt die beiden obersten Stack-Elemente `module_name` und `qualname` und pusht das Ergebnis des Nachschlagens des punktgetrennten `qualname` im Modul namens `module_name`.
  • MEMOIZE: Speichert das oberste Stack-Objekt im Memo-Dictionary unter einem Index, der der aktuellen Größe des Memo-Dictionaries entspricht.

Alternative Ideen

Prefetching

Serhiy Storchaka schlug vor, das Framing durch einen speziellen PREFETCH-Opcode (mit einem 2- oder 4-Byte-Argument) zu ersetzen, um bekannte Pickle-Chunks explizit zu deklarieren. Große Daten können außerhalb solcher Chunks gepickelt werden. Ein naiver Unpickler sollte den PREFETCH-Opcode überspringen und Pickles trotzdem korrekt dekodieren können, aber eine gute Fehlerbehandlung erfordert die Überprüfung, ob die PREFETCH-Länge auf einer Opcode-Grenze liegt.

Danksagungen

Alphabetisch geordnet

  • Alexandre Vassalotti für den Start der zweiten PEP 3154-Implementierung [6]
  • Serhiy Storchaka für die Diskussion des Framing-Vorschlags [6]
  • Stefan Mihaila für den Start der ersten PEP 3154-Implementierung als Google Summer of Code-Projekt unter der Betreuung von Alexandre Vassalotti [7].

Referenzen


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

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