[PYTHON] Résumé du traitement pickle et unpickle des classes définies par l'utilisateur

Aperçu

pickle est un mécanisme très puissant dans le format de sérialisation de données unique de Python, mais son comportement est moins flexible et plus simple que par le passé. Ici, nous résumons le processus de décapage et de décapage des classes intégrées non principales (classes intégrées mineures, bibliothèques standard / non standard, classes définies par l'utilisateur, etc.), et comment décaper efficacement les utilisateurs. J'ai résumé si une classe de définition peut être créée.

La discussion ici est basée sur Python 3.3.5 et Pickle Protocol Version 3. La version 4 du protocole a été introduite à partir de Python 3.4, mais comme le traitement interne est devenu plus compliqué, je pense qu'il serait efficace de le comprendre d'abord avec le code de Python 3.3.

Flux de traitement des cornichons

Principalement, vous pouvez comprendre en suivant la méthode suivante.

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ère étape de cornichon

Lorsque pickle.dump, pickle.dumps, etc. sont appelés, tout est converti en pickle par le traitement suivant.

sample1.py


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

La classe Pickler est

  1. Implémentation C _pickle.Pickler, ou
  2. Implémentation Python pickle._Pickler Donc, il y a des entités aux endroits suivants.
  3. static PyTypeObject Pickler_Type; défini dans Modules / _pickler.c
  4. class _Pickler définie dans Lib / pickle.py Normalement, l'implémentation C est utilisée de préférence, mais si l'importation échoue, l'implémentation Python est utilisée. Puisque le but principal ici est de comprendre le mécanisme, nous nous concentrerons sur l'implémentation Python.

Les objets individuels sont décapés récursivement par pickler.save (obj). Premièrement, les objets existants tels que les références circulaires et les références à plusieurs emplacements sont convenablement décapés comme références directes dans la première moitié de cette fonction.

Pour les principales classes intégrées

Comme les classes et les constantes intégrées ci-dessous sont souvent utilisées, Pickle implémente son propre traitement efficace. Pour cette raison, il ne correspond pas à l'explication de cet article et est omis ici. int, float, str, bytes, list, tuple, dict, bool, None Pour les autres classes, il est décapé par la procédure ci-dessous.

Pour les objets ou fonctions de classe

Si la cible pickle est un objet de classe (c'est-à-dire ʻis instance (obj, type) == True) ou une fonction, ʻobj .__ module__, obj .__ nom__ est enregistré comme une chaîne de caractères. Lors de la conversion sans sélection, après l'importation du module requis, la valeur à laquelle on peut faire référence par ce nom de variable est décochée. Autrement dit, seules les classes et les fonctions définies sur l'espace de noms global du module peuvent être picklées. Bien sûr, la logique des fonctions et des classes n'est pas mémorisée, Python n'est pas LISP.

Pour les objets de classes enregistrés dans le module copyreg

Ensuite, l'existence de copyreg.dispatch_table [type (obj)] est vérifiée à partir du dictionnaire défini globalement dans le module copyreg.

sample02.py


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

Le contenu de la valeur de retour «rv» sera décrit plus tard.

De cette façon, la fonction enregistrée dans copyreg.dispatch_table a la priorité la plus élevée et est utilisée pour le pickleization. Par conséquent, même une classe dont la définition ne peut pas être modifiée peut changer le comportement de pickle / unpickle. Dans un cas extrême, si vous faites un pickle / unpickle un objet temps, vous pouvez en faire un objet d'expression régulière.

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]+')Sortie comme

L'ajout au dictionnaire dispatch_table se fait via copyreg.pickle (type, func).

S'il existe un dictionnaire pickler.dispatch_table, celui-ci sera utilisé à la place de copyreg.dispatch_table. Ceci est plus sûr si vous souhaitez modifier le comportement uniquement lors du décapage dans un but précis.

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]+')Sortie comme

Si ʻobj .__ reduction_ex__` est défini

Si la méthode ʻobj .__ reduction_ex__` est définie

sample03.py


rv = obj.__reduce_ex__(protocol_version)

Est appelé. Le contenu de la valeur de retour «rv» sera décrit plus tard.

Si ʻobj .__ reduction__` est défini

Si la méthode ʻobj .__ reduction__` est définie,

sample03.py


rv = obj.__reduce__()

Est appelé. Le contenu de la valeur de retour «rv» sera décrit plus tard.

Besoin de __reduce__

Il semble que ce ne soit pas la situation actuelle. Vous devez toujours utiliser «reduce_ex». Ceci est recherché en premier, donc ce sera un peu plus rapide. Si vous n'utilisez pas la variable de protocole, vous pouvez l'ignorer.

Si vous n'avez pas de définition particulière

Si aucune méthode spéciale n'est écrite pour pickle / unpickle, le traitement ʻobject standard reduction est effectué en dernier recours. C'est, pour ainsi dire, "l'implémentation la plus universelle et la plus maximale de reduce_ex` qui peut être utilisée telle quelle pour la plupart des objets ", ce qui est très utile, mais malheureusement elle est implémentée en langage C et compréhension difficile. Si cette partie est omise comme la gestion des erreurs et que le flux général est implémenté en Python, ce sera comme suit.

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

Comme vous pouvez le voir ci-dessus, même si vous comptez sur ʻobject .__ reduction_ex__, vous pouvez changer le comportement en détail en définissant les méthodes de getnewargs, getstate. Si vous définissez vous-même reduce_ex, reduce`, ces fonctions ne seront pas utilisées à moins que vous ne les appeliez explicitement.

__getnewargs__ Une méthode qui renvoie un taple qui peut être mariné. Une fois que cela est défini, les arguments de __new__ dans le décapage (et non de __init__) peuvent être personnalisés. N'inclut pas le premier argument (objet de classe).

__getstate__ Si cela est défini, l'argument de __setstate__ lors de la suppression du pic, ou la valeur initiale de __dict__ et de l'emplacement lorsque __setstate__ n'existe pas peut être personnalisé.

Valeurs qui doivent être renvoyées par les fonctions d'enregistrement __reduce_ex__, __reduce__ et copyreg

Dans le processus ci-dessus, la valeur rv que chaque fonction doit renvoyer est

Est.

Si type (rv) est str

type (obj) .__ module__, rv est enregistré comme une chaîne dans la conversion pickle, et la conversion unpickle renvoie l'objet référencé par ce nom une fois le module correctement importé. Ce mécanisme peut être utilisé efficacement lors du décapage d'un objet singleton ou similaire.

Si type (rv) est tuple

Les éléments du taple (2 ou plus et 5 ou moins) sont les suivants

  1. func --Un objet sélectionnable et appelable (généralement un objet de classe) qui crée un objet lorsqu'il n'est pas sélectionné. Cependant, dans le cas de func .__ name__ ==" __newobj__ ", il sera décrit plus tard avec une exception.
  2. ʻargs--pickle Un taple d'éléments possibles. Utilisé comme paramètre lors de l'appel defunc`.
  3. state - Un objet pour décocher l'état d'un objet. Optionnel. Cela peut être "Aucun".
  4. listitems - un objet itérable qui renvoie les éléments d'un objet de type liste. Optionnel. Cela peut être "Aucun".
  5. dictitems - un objet itérable qui renvoie les clés et les éléments d'un objet de typedict. La valeur renvoyée par l'itérateur doit être une paire clé / élément. Typiquement dict_object.items (). Optionnel. Cela peut être "Aucun".

Pour func .__ name__ ==" __newobj__ "

Dans ce cas, ʻargs [0] est interprété comme un objet de classe et un objet de classe est créé avec ʻargs comme argument. À ce stade, «init» n'est pas appelé. Si vous avez besoin d'un objet func avec ces conditions, vous en avez déjà un déclaré dans le module copyreg.

Lib/copyreg.py


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

Ce copyreg .__ newobj__ est implémenté et entré de façon à ce qu'il se comporte de la même manière même s'il est interprété comme une fonction normale, mais il n'est pas réellement exécuté.

Interprétation de la valeur de «état»

Il est interprété comme suit.

  1. Si l'objet à décocher a «obj .__ setstate__», l'argument de cette méthode.
  2. Pour les taples de l'élément 2, state [0] est un dictionnaire qui indique le contenu de ʻobj .__ items__, et state [1] est un dictionnaire qui indique le contenu de type (obj) .__ slots__`. Les deux peuvent être "Aucun".
  3. Pour un dictionnaire unique, le contenu de ʻobj .__ items__`

Déchiqueter le flux de processus

Principalement, vous pouvez comprendre en suivant la méthode suivante.

Lib/pickle.py


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

1ère étape de décoller

Lorsque pickle.load, pickle.loads, etc. sont appelés, tous sont décochés par le traitement suivant.

sample1.py


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

La classe Unpickler est

  1. Implémentation C _pickle.Unpickler, ou
  2. Implémentation Python pickle._Unpickler Donc, il y a des entités aux endroits suivants.
  3. static PyTypeObject Unpickler_Type; défini dans Modules / _pickler.c
  4. class _Unpickler définie dans Lib / pickle.py

Selon l'élément dans les données pickle, selon l'ID appelé opcode, l'objet est restauré en appelant séquentiellement ʻunpickler.load_xxx () `.

Décollez les données d'opcode globales

Dans les cas où une classe, une fonction ou __reduce_ex__ retourne une chaîne, la chaîne`" modulename.varname "ʻest enregistrée telle quelle. Dans ce cas, importez le module si nécessaire et sortez la valeur correspondante. Aucun nouvel objet n'est créé par unpickler.

décoller newobj, réduire, créer des données opcode

Lorsqu'il est décapé en utilisant le tapple à 5 éléments retourné par __reduce_ex__ etc., l'objet est décoché par ces processus. Si vous réécrivez le contour de chaque méthode de load_newobj, load_reduce, load_build correspondant à ce processus dans un flux simple, ce sera comme suit.

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

Étude de cas

Cas où vous n'avez rien à faire

Les cas qui satisfont aux conditions suivantes peuvent être traités de manière appropriée sans écrire les processus pickle et unpickle.

  1. Le contenu de tous les __dict__ peut être décapé, et il n'y a aucun problème même s'ils sont restaurés tels quels.
  2. Les valeurs d'attribut correspondant à tous les __slots__ peuvent être picklées, et il n'y a pas de problème même si elles sont restaurées telles quelles.
  3. En raison de l'implémentation du langage C, il n'a pas de données internes inaccessibles à partir de Python.
  4. Aucun traitement pour interpréter l'argument n'est ajouté à «new».
  5. Même si «init» n'est pas appelé, il n'y a pas d'incohérence en tant qu'objet si les attributs sont restaurés correctement.
  6. Dans le cas des sous-classes de list et dict, tous les éléments peuvent être décapés et restaurés tels quels sans aucun problème.

Objets avec des attributs que vous ne souhaitez pas inclure dans pickle (comme le cache) ou des attributs qui ne peuvent pas être pickle

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()

Lorsque l'objet Shere qui représente une sphère accède à la propriété de volume qui représente le volume, le résultat du calcul est mis en cache en interne. Si cela est mariné tel quel, le volume mis en cache sera enregistré ensemble et le tiers des données augmentera. Je veux supprimer ceci.

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}

Vous pouvez empêcher le cache d'être picklé en définissant une méthode __getstate__ qui retourne la valeur de __dict __ après unpickle.

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}

Pour améliorer l'efficacité de la mémoire, si vous définissez __slots__, __dict__ n'existera plus et vous devrez changer la valeur renvoyée par __getstate__. Dans ce cas, il s'agit d'un taple à deux éléments, et le dernier élément est un dictionnaire qui initialise les attributs de «slots». L'élément précédent (valeur initiale de «dict») peut être «Aucun».

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

Si la seule valeur à choisir est le rayon, vous pouvez renvoyer la valeur self._radius elle-même sous la forme __getstate__ au lieu du dictionnaire. Dans ce cas, définissez également un __setstate__ apparié.

Objets qui ne peuvent pas être créés sans donner les arguments appropriés à __new__

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()

Objets qui ne peuvent pas être créés sans appeler __init__

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()

La valeur retournée par get est stockée par la fermeture dans __init__, donc l'objet ne peut pas être créé sans appeler __init__. Dans un tel cas, ʻobject .__ reduction_ex__ne peut pas être utilisé, donc implémentezreduce_ex` par vous-même.

Objet Singleton

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()

Supposons que la classe MySingleton n'a qu'une seule instance dans la variable globale mysingleton. Pour décocher cela correctement, utilisez un format dans lequel __reduce_ex__ renvoie une chaîne.

Recommended Posts

Résumé du traitement pickle et unpickle des classes définies par l'utilisateur
Récapitulatif du traitement de la date en Python (datetime et dateutil)
Calcul de la classe auto-fabriquée et de la classe existante
Résumé des index et des tranches Python
Résumé du traitement multi-processus du langage de script
Exemple d'utilisation de variables de classe et de méthodes de classe
Réponses et impressions de 100 chocs de traitement de la langue - Partie 2
Comparaison de l'héritage de classe et de la description du constructeur
Résumé de la correspondance entre les opérations de tableau ruby et python
Résumé des outils et bibliothèques OSS créés en 2016
Résumé des différences entre PHP et Python
Installation de Python 3 et Flask [Résumé de la construction de l'environnement]
Résumé relatif aux E / S de python et fortran
[Python] Type de classe et utilisation du module datetime
Classe de traitement de page
[Traitement du langage 100 coups 2020] Résumé des exemples de réponses par Python
Traitement pleine largeur et demi-largeur des données CSV en Python
Python - Explication et résumé de l'utilisation des 24 meilleurs packages
[Python] Erreur de type: résumé des causes et des solutions pour "Aucun type"
Traitement asynchrone de Python ~ Comprenez parfaitement async et attendez ~
[Kaggle] Récapitulatif du prétraitement (statistiques, traitement des valeurs manquantes, etc.)
Exemple d'obtention du nom du module et du nom de la classe en Python
Vue d'ensemble des vues génériques basées sur les classes et des relations de classe héritées
Vue d'ensemble du traitement du langage naturel et de son prétraitement des données