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.
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)
{
...
}
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
_pickle.Pickler
, oupickle._Pickler
Donc, il y a des entités aux endroits suivants.static PyTypeObject Pickler_Type;
défini dans Modules / _pickler.cclass _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.
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.
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.
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 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 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.
__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 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é.
__reduce_ex__, __reduce__
et copyregDans le processus ci-dessus, la valeur rv
que chaque fonction doit renvoyer est
Est.
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.
type (rv) est tuple
Les éléments du taple (2 ou plus et 5 ou moins) sont les suivants
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.--pickle Un taple d'éléments possibles. Utilisé comme paramètre lors de l'appel de
func`.state
- Un objet pour décocher l'état d'un objet. Optionnel. Cela peut être "Aucun".listitems
- un objet itérable qui renvoie les éléments d'un objet de type liste
. Optionnel. Cela peut être "Aucun".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".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é.
Il est interprété comme suit.
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".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):
...
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
_pickle.Unpickler
, oupickle._Unpickler
Donc, il y a des entités aux endroits suivants.static PyTypeObject Unpickler_Type;
défini dans Modules / _pickler.cclass _Unpickler
définie dans Lib / pickle.pySelon l'élément dans les données pickle, selon l'ID appelé opcode, l'objet est restauré en appelant séquentiellement ʻunpickler.load_xxx () `.
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.
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
Les cas qui satisfont aux conditions suivantes peuvent être traités de manière appropriée sans écrire les processus pickle et unpickle.
__dict__
peut être décapé, et il n'y a aucun problème même s'ils sont restaurés tels quels.__slots__
peuvent être picklées, et il n'y a pas de problème même si elles sont restaurées telles quelles.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é.
__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()
__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émentez
reduce_ex` par vous-même.
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