[PYTHON] Zusammenfassung der Pickle- und Unpickle-Verarbeitung von benutzerdefinierten Klassen

Überblick

pickle ist ein sehr leistungsfähiger Mechanismus in Pythons einzigartigem Daten-Serialisierungsformat, aber das Verhalten dahinter ist weniger flexibel und einfacher als in der Vergangenheit. Hier fassen wir den Prozess des Beizens und Entpickelns von nicht integrierten Hauptklassen (kleinere eingebaute Klassen, Standard- / Nicht-Standardbibliotheken, benutzerdefinierte Klassen usw.) und das effiziente Beizen von Benutzern zusammen. Ich habe zusammengefasst, ob eine Definitionsklasse erstellt werden kann.

Die Diskussion hier basiert auf Python 3.3.5 und Pickle Protocol Version 3. Das Protokoll Version 4 wurde aus Python 3.4 eingeführt, aber die interne Verarbeitung ist komplizierter geworden. Ich denke, es wäre effizient, es zuerst mit dem Code von Python 3.3 zu verstehen.

Ablauf der Essiggurkenverarbeitung

Sie können dies hauptsächlich anhand der folgenden Methode verstehen.

Lib/pickle.py


class _Pickler:
    def save(self, obj, save_persistent_id=True):
        ...
    def save_reduce(self, func, args, state=None,
                    listitems=None, dictitems=None, obj=None):
        ...
    def save_global(self, obj, name=None, pack=struct.pack):
        ...

Objects/typeobject.c


static PyObject *
reduce_2(PyObject *obj)
{
    ...
}

static PyObject *
object_reduce_ex(PyObject *self, PyObject *args)
{
    ...
}

1. Schritt der Gurke

Wenn pickle.dump, pickle.dumps usw. aufgerufen werden, wird alles durch die folgende Verarbeitung in pickle konvertiert.

sample1.py


pickler = pickle.Pickler(fileobj, protocol)
pickler.dump(obj)

Die Pickler-Klasse ist

  1. C-Implementierung _pickle.Pickler oder
  2. Python-Implementierung pickle._Pickler Es gibt also Entitäten an den folgenden Stellen.
  3. static PyTypeObject Pickler_Type; definiert in Modules / _pickler.c
  4. class _Pickler definiert in Lib / pickle.py Normalerweise wird die C-Implementierung bevorzugt verwendet. Wenn der Import jedoch fehlschlägt, wird die Python-Implementierung verwendet. Da der Hauptzweck hier darin besteht, den Mechanismus zu verstehen, konzentrieren wir uns auf die Python-Implementierung.

Einzelne Objekte werden von pickler.save (obj) rekursiv gebeizt. Zunächst werden vorhandene Objekte wie Zirkelreferenzen und Referenzen an mehreren Stellen in der ersten Hälfte dieser Funktion entsprechend als Vorwärtsreferenzen ausgewählt.

Für große eingebaute Klassen

Da die folgenden integrierten Klassen und Konstanten häufig verwendet werden, implementiert Pickle eine eigene effiziente Verarbeitung. Aus diesem Grund entspricht es nicht der Erklärung in diesem Artikel und wird hier weggelassen. int, float, str, bytes, list, tuple, dict, bool, None Für andere Klassen wird es nach dem unten gezeigten Verfahren eingelegt.

Für Klassenobjekte oder Funktionen

Wenn das Pickle-Ziel ein Klassenobjekt (dh "isinstance (obj, type) == True") oder eine Funktion ist, wird "obj .__ module__, obj .__ name__" als Zeichenfolge aufgezeichnet. Bei der Unpickle-Konvertierung wird nach dem Import des erforderlichen Moduls der Wert, auf den dieser Variablenname verweisen kann, aufgehoben. Das heißt, nur Klassen und Funktionen, die im globalen Namespace des Moduls definiert sind, können ausgewählt werden. Natürlich wird die Logik von Funktionen und Klassen nicht berücksichtigt, Python ist nicht LISP.

Für Objekte von Klassen, die im copyreg-Modul registriert sind

Als nächstes wird die Existenz von "copyreg.dispatch_table [type (obj)]" aus dem im copyreg-Modul global definierten Wörterbuch überprüft.

sample02.py


import copyreg
if type(obj) in copyreg.dispatch_table:
    reduce = copyreg.dispatch_table[type(obj)]
    rv = reduce(obj)

Der Inhalt des Rückgabewerts "rv" wird später beschrieben.

Auf diese Weise hat die in "copyreg.dispatch_table" registrierte Funktion die höchste Priorität und wird zur Beizung verwendet. Daher kann auch eine Klasse, deren Definition nicht geändert werden kann, das Verhalten von pickle / unpickle ändern. Im Extremfall können Sie ein Zeitobjekt zum Ein- / Ausheben eines Zeitobjekts machen, indem Sie es zu einem Objekt mit regulären Ausdrücken machen.

sample03.py


import pickle
import copyreg
import datetime
import re

def reduce_datetime_to_regexp(x):
    return re.compile, (r'[spam]+',)

copyreg.pickle(datetime.datetime, reduce_datetime_to_regexp)

a = datetime.datetime.now()
b = pickle.loads(pickle.dumps(a))
print(a, b) # 2014-10-05 10:24:12.177959 re.compile('[spam]+')Ausgabe wie

Das Hinzufügen zum Wörterbuch dispatch_table erfolgt über copyreg.pickle (type, func).

Wenn es ein Wörterbuch "pickler.dispatch_table" gibt, wird dieses anstelle von "copyreg.dispatch_table" verwendet. Dies ist sicherer, wenn Sie das Verhalten nur beim Beizen für einen bestimmten Zweck ändern möchten.

sample03a.py


import pickle
import copyreg
import datetime
import re
import io

def reduce_datetime_to_regexp(x):
    return re.compile, (r'[spam]+',)

a = datetime.datetime.now()

with io.BytesIO() as fp:
    pickler = pickle.Pickler(fp)
    pickler.dispatch_table = copyreg.dispatch_table.copy()
    pickler.dispatch_table[datetime.datetime] = reduce_datetime_to_regexp
    pickler.dump(a)
    b = pickle.loads(fp.getvalue())

print(a, b) # 2014-10-05 10:24:12.177959 re.compile('[spam]+')Ausgabe wie

Wenn obj .__ redu_ex__ definiert ist

Wenn die Methode obj .__ redu_ex__ definiert ist

sample03.py


rv = obj.__reduce_ex__(protocol_version)

Wird genannt. Der Inhalt des Rückgabewerts "rv" wird später beschrieben.

Wenn obj .__ redu____ definiert ist

Wenn die Methode obj .__ redu____ definiert ist

sample03.py


rv = obj.__reduce__()

Wird genannt. Der Inhalt des Rückgabewerts "rv" wird später beschrieben.

Notwendigkeit für "reduce"

Es scheint, dass es nicht die aktuelle Situation ist. Sie sollten immer __reduce_ex__ verwenden. Dies wird zuerst gesucht, damit es etwas schneller geht. Wenn Sie die Protokollvariable nicht verwenden, können Sie sie ignorieren.

Wenn Sie keine spezielle Definition haben

Wenn keine spezielle Methode für Pickle / Unpickle geschrieben wurde, wird als letztes Mittel der "Objekt" -Standard "-Reduktions" -Prozess durchgeführt. Dies ist sozusagen "die universellste und maximalste Commitment-Implementierung" reduce_ex ", die wie für die meisten Objekte verwendet werden kann", was sehr hilfreich ist, aber leider in C-Sprache und -Verständnis implementiert ist schwer. Wenn dieser Teil wie die Fehlerbehandlung weggelassen wird und der allgemeine Ablauf in Python implementiert ist, sieht er wie folgt aus.

object_reduce_ex.py


class object:
    def __reduce_ex__(self, proto):
        from copyreg import __newobj__

        if hasattr(self, '__getnewargs__'):
            args = self.__getnewargs__()
        else:
            args = ()

        if hasattr(self, '__getstate__'):
            state = self.__getstate__()
        elif hasattr(type(self), '__slots__'):
            state = self.__dict__, {k: getattr(self, k) for k in type(self).__slots__}
        else:
            state = self.__dict__

        if isinstance(self, list):
            listitems = self
        else:
            listitems = None

        if isinstance(self, dict):
            dictitems = self.items()
        else:
            listitems = None

        return __newobj__, (type(self),)+args, state, listitems, dictitems

Wie Sie oben sehen können, können Sie das Verhalten im Detail ändern, indem Sie die Methoden von getnewargs, getstatedefinieren, auch wenn Sie sich aufobject .__ redu_ex__` verlassen. Wenn Sie "reduce_ex", "reduce" selbst definieren, werden diese Funktionen nur verwendet, wenn Sie sie explizit aufrufen.

__getnewargs__ Eine Methode, die einen Taple zurückgibt, der eingelegt werden kann. Sobald dies definiert ist, können die Argumente für "new" bei der Unpickleization (nicht "init") angepasst werden. Enthält nicht das erste Argument (Klassenobjekt).

__getstate__ Wenn dies definiert ist, kann das Argument von "setstate" bei der Unpickleization oder der Anfangswert von "dict" und der Slot, wenn "setstate" nicht vorhanden ist, angepasst werden.

Werte, die von den Registrierungsfunktionen __reduce_ex__, __reduce__ und copyreg zurückgegeben werden sollen

In dem obigen Prozess ist der Wert "rv", den jede Funktion zurückgeben sollte

Ist.

Wenn type (rv) str ist

type (obj) .__ module__, rv wird bei der Pickle-Konvertierung als Zeichenfolge aufgezeichnet. Bei der Unpickle-Konvertierung wird das Modul ordnungsgemäß importiert und das Objekt, auf das dieser Name verweist, wird zurückgegeben. Dieser Mechanismus kann effektiv verwendet werden, wenn ein Singleton-Objekt oder dergleichen gebeizt wird.

Wenn type (rv) tuple ist

Die Elemente des Taples (2 oder mehr und 5 oder weniger) sind wie folgt

  1. func - Ein auswählbares und aufrufbares Objekt (normalerweise ein Klassenobjekt), das beim Aufheben der Auswahl ein Objekt erstellt. Im Fall von func .__ name__ ==" __newobj__ " wird die Ausnahme jedoch später beschrieben.
  2. args --pickle Ein Taple möglicher Elemente. Wird als Parameter beim Aufruf von func verwendet.
  3. state - Ein Objekt zum Aufheben der Auswahl des Status eines Objekts. Optional. Es kann "Keine" sein.
  4. listitems - ein iterierbares Objekt, das Elemente eines list-ähnlichen Objekts zurückgibt. Optional. Es kann "Keine" sein.
  5. dictitems - ein iterierbares Objekt, das die Schlüssel und Elemente einesdict-ähnlichen Objekts zurückgibt. Der vom Iterator zurückgegebene Wert muss ein Schlüssel / Element-Paar sein. Normalerweise "dict_object.items ()". Optional. Es kann "Keine" sein.

Für func .__ name__ ==" __newobj__ "

In diesem Fall wird "args [0]" als Klassenobjekt interpretiert und ein Klassenobjekt mit "args" als Argument erstellt. Zu diesem Zeitpunkt wird __init__ nicht aufgerufen. Wenn Sie ein func-Objekt mit diesen Bedingungen benötigen, haben Sie bereits eines im copyreg-Modul deklariert.

Lib/copyreg.py


def __newobj__(cls, *args):
    return cls.__new__(cls, *args)

Dieses copyreg .__ newobj__ wird implementiert und eingegeben, so dass es sich genauso verhält, auch wenn es als normale Funktion interpretiert wird, aber nicht tatsächlich ausgeführt wird.

Interpretation des Wertes von "Zustand"

Es wird wie folgt interpretiert.

  1. Wenn das zu entpackende Objekt "obj .__ setstate__" hat, das Argument für diese Methode.
  2. Für Element 2-Taples ist "state [0]" ein Wörterbuch, das den Inhalt von "obj .__ items__" angibt, und "state [1]" ist ein Wörterbuch, das den Inhalt von "type (obj) .__ slots__" angibt. Beide können "Keine" sein.
  3. Für ein einzelnes Wörterbuch den Inhalt von obj .__ items__

Prozessablauf aufheben

Sie können dies hauptsächlich anhand der folgenden Methode verstehen.

Lib/pickle.py


class _Unpickler:
    def load_newobj(self):
        ...
    def load_reduce(self):
        ...
    def load_build(self):
        ...
    def load_global(self):
        ...

1. Schritt des Abhebens

Wenn pickle.load, pickle.loads usw. aufgerufen werden, wird durch die folgende Verarbeitung alles aufgehoben.

sample1.py


unpickler = pickle.Unpickler(fileobj)
unpickler.load()

Die Unpickler-Klasse ist

  1. C-Implementierung _pickle.Unpickler oder
  2. Python-Implementierung pickle._Unpickler Es gibt also Entitäten an den folgenden Stellen.
  3. static PyTypeObject Unpickler_Type; definiert in Modules / _pickler.c
  4. class _Unpickler definiert in Lib / pickle.py

Das Objekt wird wiederhergestellt, während nacheinander "unpickler.load_xxx ()" gemäß der ID aufgerufen wird, die als Opcode bezeichnet wird, entsprechend dem Element in den Pickle-Daten.

Globale Opcode-Daten entfernen

In Fällen, in denen eine Klasse, Funktion oder __reduce_ex__ eine Zeichenfolge zurückgibt, wird die Zeichenfolge" modulename.varname " unverändert aufgezeichnet. Importieren Sie in diesem Fall ggf. das Modul und geben Sie den entsprechenden Wert aus. Unpickler erstellt kein neues Objekt.

Newobj entfernen, reduzieren, Opcode-Daten erstellen

Bei der Beizung mit dem von __reduce_ex__ usw. zurückgegebenen 5-Element-Tapple wird das Objekt durch diese Prozesse entpickt. Wenn Sie den Umriss jeder Methode von "load_newobj, load_reduce, load_build", die diesem Prozess entspricht, in einem einfachen Ablauf neu schreiben, sieht dies wie folgt aus.

sample09.py


def unpickle_something():
    func, args, state, listitems, dictitems = load_from_pickle_stream()

    if getattr(func, '__name__', None) == '__newobj__':
        obj = args[0].__new__(*args)
    else:
        obj = func(*args)

    if lisitems is not None:
        for x in listitems:
            obj.append(x)

    if dictitems is not None:
        for k, v in dictitems:
            obj[k] = v

    if hasattr(obj, '__setstate__'):
        obj.__setstate__(state)
    elif type(state) is tuple and len(state) == 2:
        for k, v in state[0].items():
            obj.__dict__[k] = v
        for k, v in state[1].items():
            setattr(obj, k, v)
    else:
        for k, v in state.items():
            obj.__dict__[k] = v

    return obj

Fallstudie

Fall, in dem Sie nichts tun müssen

Fälle, die die folgenden Bedingungen erfüllen, können ordnungsgemäß verarbeitet werden, ohne die Pickle- und Unpickle-Prozesse zu schreiben.

  1. Der Inhalt aller "dict" kann eingelegt werden, und es gibt kein Problem, selbst wenn sie so wiederhergestellt werden, wie sie sind.
  2. Die Attributwerte, die allen "slots" entsprechen, können ausgewählt werden, und es gibt kein Problem, selbst wenn sie so wiederhergestellt werden, wie sie sind.
  3. Aufgrund der Implementierung der C-Sprache sind keine internen Daten verfügbar, auf die über Python nicht zugegriffen werden kann.
  4. Es wird keine Verarbeitung zur Interpretation des Arguments zu "new" hinzugefügt.
  5. Auch wenn __init__ nicht aufgerufen wird, gibt es keine Inkonsistenz als Objekt, wenn die Attribute korrekt wiederhergestellt werden.
  6. Bei Unterklassen von Liste und Diktat können alle Elemente problemlos eingelegt und wiederhergestellt werden.

Objekte mit Attributen, die Sie nicht in pickle aufnehmen möchten (z. B. Cache), oder Attribute, die nicht pickle sein können

sphere0.py


import pickle

class Sphere:
    def __init__(self, radius):
        self._radius = radius
    @property
    def volume(self):
        if not hasattr(self, '_volume'):
            from math import pi
            self._volume = 4/3 * pi * self._radius ** 3
        return self._volume

def _main():
    sp1 = Sphere(3)
    print(sp1.volume)
    print(sp1.__reduce_ex__(3))
    sp2 = pickle.loads(pickle.dumps(sp1))
    print(sp2.volume)

if __name__ == '__main__':
    _main()

Wenn das Shere-Objekt, das eine Kugel darstellt, auf die Volume-Eigenschaft zugreift, die das Volume darstellt, wird das Berechnungsergebnis intern zwischengespeichert. Wenn dies so wie es ist eingelegt wird, wird das zwischengespeicherte Volume zusammen gespeichert und der dritte Datenwert erhöht sich. Ich möchte dies löschen.

sphere1.py


class Sphere:
    def __init__(self, radius):
        self._radius = radius
    @property
    def volume(self):
        if not hasattr(self, '_volume'):
            from math import pi
            self._volume = 4/3 * pi * self._radius ** 3
        return self._volume
    def __getstate__(self):
        return {'_radius': self._radius}

Sie können verhindern, dass der Cache eingelegt wird, indem Sie eine "getstate" -Methode definieren, die nach dem Aufheben der Auswahl den Wert von "__dict __" zurückgibt.

sphere2.py


class Sphere:
    __slots__ = ['_radius', '_volume']
    def __init__(self, radius):
        self._radius = radius
    @property
    def volume(self):
        if not hasattr(self, '_volume'):
            from math import pi
            self._volume = 4/3 * pi * self._radius ** 3
        return self._volume
    def __getstate__(self):
        return None, {'_radius': self._radius}

Wenn Sie "slots" definieren, ist "dict" nicht mehr vorhanden und Sie müssen den von "getstate" zurückgegebenen Wert ändern, um die Speichereffizienz zu verbessern. In diesem Fall handelt es sich um einen Taple mit zwei Elementen, und das letztere Element ist ein Wörterbuch, das die Attribute von __slots__ initialisiert. Das vorherige Element (Anfangswert von __dict__) kann None sein.

sphere3.py


class Sphere:
    __slots__ = ['_radius', '_volume']
    def __init__(self, radius):
        self._radius = radius
    @property
    def volume(self):
        if not hasattr(self, '_volume'):
            from math import pi
            self._volume = 4/3 * pi * self._radius ** 3
        return self._volume
    def __getstate__(self):
        return self._radius
    def __setstate__(self, state):
        self._radius = state

Wenn der einzige Wert, der ausgewählt werden soll, der Radius ist, können Sie den Wert "self._radius" selbst anstelle des Wörterbuchs als "getstate" zurückgeben. Definieren Sie in diesem Fall auch einen gepaarten __setstate__.

Objekte, die nicht erstellt werden können, ohne __new__ entsprechende Argumente zu geben

intliterals.py


import pickle

class IntLiterals(tuple):
    def __new__(cls, n):
        a = '0b{n:b} 0o{n:o} {n:d} 0x{n:X}'.format(n=n).split()
        return super(cls, IntLiterals).__new__(cls, a)
    def __getnewargs__(self):
        return int(self[0], 0),

def _main():
    a = IntLiterals(10)
    print(a) # ('0b1010', '0o12', '10', '0xA')
    print(a.__reduce_ex__(3))
    b = pickle.loads(pickle.dumps(a))
    print(b)

if __name__ == '__main__':
    _main()

Objekte, die nicht erstellt werden können, ohne __init__ aufzurufen

closureholder.py


import pickle

class ClosureHolder:
    def __init__(self, value):
        def _get():
            return value
        self._get = _get
    def get(self):
        return self._get()
    def __reduce_ex__(self, proto):
        return type(self), (self.get(),)

def _main():
    a = ClosureHolder('spam')
    print(a.get())
    print(a.__reduce_ex__(3))
    b = pickle.loads(pickle.dumps(a))
    print(b.get())

if __name__ == '__main__':
    _main()

Der von get zurückgegebene Wert wird beim Schließen in __init__ gespeichert, sodass das Objekt nicht ohne Aufruf von __init__ erstellt werden kann. In einem solchen Fall kann object .__ redu_ex__ nicht verwendet werden, also implementieren Sie__reduce_ex__ selbst.

Singleton-Objekt

singleton.py


class MySingleton(object):
    def __new__(cls, *args, **kwds):
        assert mysingleton is None, \
            'A singleton of MySingleton has already been created.'
        return super(cls, MySingleton).__new__(cls, *args, **kwds)
    def __reduce_ex__(self, proto):
        return 'mysingleton'

mysingleton = None
mysingleton = MySingleton()

def _main():
    import pickle
    a = pickle.dumps(mysingleton)
    b = pickle.loads(a)
    print(b)

if __name__ == '__main__':
    _main()

Angenommen, die MySingleton-Klasse hat nur eine Instanz in der globalen Variablen mysingleton. Um dies korrekt aufzuheben, verwenden Sie ein Format, in dem __reduce_ex__ eine Zeichenfolge zurückgibt.

Recommended Posts

Zusammenfassung der Pickle- und Unpickle-Verarbeitung von benutzerdefinierten Klassen
Zusammenfassung der Datumsverarbeitung in Python (Datum / Uhrzeit und Datum)
Berechnung der selbst erstellten Klasse und der vorhandenen Klasse
Zusammenfassung der Python-Indizes und -Slices
Zusammenfassung der Mehrprozessverarbeitung der Skriptsprache
Beispiel für die Verwendung von Klassenvariablen und Klassenmethoden
Antworten und Eindrücke von 100 Sprachverarbeitungsklopfen - Teil 2
Vergleich von Klassenvererbung und Konstruktorbeschreibung
Zusammenfassung der Korrespondenz zwischen Ruby- und Python-Array-Operationen
Zusammenfassung der 2016 erstellten OSS-Tools und -Bibliotheken
Zusammenfassung der Unterschiede zwischen PHP und Python
Installation von Python 3 und Flask [Zusammenfassung der Umgebungskonstruktion]
E / A-bezogene Zusammenfassung von Python und Fortan
[Python] Klassentyp und Verwendung des datetime-Moduls
Seitenverarbeitungsklasse
[Sprachverarbeitung 100 Schläge 2020] Zusammenfassung der Antwortbeispiele von Python
Verarbeitung von CSV-Daten in voller und halber Breite in Python
Python - Erläuterung und Zusammenfassung der Verwendung der 24 wichtigsten Pakete
[Python] Typfehler: Zusammenfassung der Ursachen und Abhilfemaßnahmen für 'Kein Typ'
Asynchrone Verarbeitung von Python ~ Asynchron vollständig verstehen und warten ~
[Kaggle] Zusammenfassung der Vorverarbeitung (Statistik, Verarbeitung fehlender Werte usw.)
Beispiel für das Abrufen des Modulnamens und des Klassennamens in Python
Übersicht über klassenbasierte generische Ansichten und geerbte Klassenbeziehungen
Überblick über die Verarbeitung natürlicher Sprache und ihre Datenvorverarbeitung