Note
Go to the end to download the full example code
Sérialisation#
Le notebook explore différentes façons de sérialiser des données et leurs limites.
JSON#
Le format :epkg:`JSON` est le format le plus utilisé sur internet notemmant via les :epkg:`API REST`.
Ecriture (json)#
'{"records": [{"nom": "Xavier", "pr\\u00e9nom": "Xavier", "langages": [{"nom": "C++", "age": 40}, {"nom": "Python", "age": 20}]}]}'
Lecture (json)#
{'records': [{'nom': 'Xavier', 'prénom': 'Xavier', 'langages': [{'nom': 'C++', 'age': 40}, {'nom': 'Python', 'age': 20}]}]}
Limite#
Les matrices numpy ne sont pas sérialisables facilement.
Object of type ndarray is not JSON serializable
Les classes ne sont pas sérialisables non plus facilement.
Object of type A is not JSON serializable
Pour ce faire, il faut indiquer au module json
comment convertir la classe en un ensemble de listes et dictionnaires et
la classe json.JSONEncoder
.
'{"classname": "A", "data": {"att": "e"}}'
Et la relecture avec la classe json.JSONDecoder
.
class MyDecoder(json.JSONDecoder):
def decode(self, o):
dec = json.JSONDecoder.decode(self, o)
if isinstance(dec, dict) and dec.get("classname") == "A":
return A(dec["data"]["att"])
else:
return dec
buffer = StringIO(res)
obj = json.load(buffer, cls=MyDecoder)
obj
<__main__.A object at 0x7fc136adc6d0>
Sérialisation rapide#
Le module json
est la librairie standard de Python mais comme
la sérialisation au format JSON est un besoin très fréquent,
il existe des alternative plus rapide comme :epkg:`ujson`.
data = {
"records": [
{
"nom": "Xavier",
"prénom": "Xavier",
"langages": [{"nom": "C++", "age": 40}, {"nom": "Python", "age": 20}],
}
]
}
timeit.timeit("json.dump(data, StringIO())", globals=globals(), number=100)
0.004422700000077384
timeit.timeit("ujson.dump(data, StringIO())", globals=globals(), number=100)
0.0004482999997890147
Ces deux lignes mesures l’écriture au format JSON mais il faut aussi mesurer la lecture.
0.000862000000097396
timeit.timeit("ujson.load(StringIO(res))", globals=globals(), number=100)
0.00037789999987580813
On enlève le temps passé dans la creation du buffer.
timeit.timeit("StringIO(res)", globals=globals(), number=100)
4.190000004200556e-05
Pickle#
Le module pickle
effectue la même chose mais au format binaire.
Celui-ci est propre à Python et ne peut être lu d’autres langages,
voire parfois par d’autres versions de Python.
Ecriture (pickle)#
data = {
"records": [
{
"nom": "Xavier",
"prénom": "Xavier",
"langages": [{"nom": "C++", "age": 40}, {"nom": "Python", "age": 20}],
}
]
}
b'\x80\x04\x95f\x00\x00\x00\x00\x00\x00\x00}\x94\x8c\x07records\x94]\x94}\x94(\x8c\x03nom\x94\x8c\x06Xavier\x94\x8c\x07pr\xc3\xa9nom\x94h\x05\x8c\x08langages\x94]\x94(}\x94(h\x04\x8c\x03C++\x94\x8c\x03age\x94K(u}\x94(h\x04\x8c\x06Python\x94h\x0bK\x14ueuas.'
Lecture (pickle)#
buffer = BytesIO(seq)
read = pickle.load(buffer)
read
{'records': [{'nom': 'Xavier', 'prénom': 'Xavier', 'langages': [{'nom': 'C++', 'age': 40}, {'nom': 'Python', 'age': 20}]}]}
Les classes#
A l’inverse du format JSON, les classes sont sérialisables avec
pickle
parce que le langage utilise un format très proche
de ce qu’il a en mémoire. Il n’a pas besoin de conversion supplémentaire.
b'\x80\x04\x95#\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x01A\x94\x93\x94)\x81\x94}\x94\x8c\x03att\x94\x8c\x01r\x94sb.'
buffer = BytesIO(seq)
read = pickle.load(buffer)
read
<__main__.A object at 0x7fc136b96650>
Réduire la taille#
Certaines informations sont duppliquées et il est préférable de ne pas les sérialiser deux fois surtout si elles sont voluminueuses.
class B:
def __init__(self, att):
self.att1 = att
self.att2 = att
b'\x80\x04\x95.\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x01B\x94\x93\x94)\x81\x94}\x94(\x8c\x04att1\x94\x8c\x01r\x94\x8c\x04att2\x94h\x06ub.'
Evitons maintenant de stocker deux fois le même attribut.
b'\x80\x04\x95#\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x01B\x94\x93\x94)\x81\x94}\x94\x8c\x03att\x94\x8c\x01r\x94sb.'
C’est plus court mais il faut inclure maintenant la relecture.
class B:
def __init__(self, att):
self.att1 = att
self.att2 = att
def __getstate__(self):
return dict(att=self.att1)
def __setstate__(self, state):
setattr(self, "att1", state["att"])
setattr(self, "att2", state["att"])
buffer = BytesIO(seq)
read = pickle.load(buffer)
read
<__main__.B object at 0x7fc136b95b70>
('r', 'r')
data = B("r")
timeit.timeit("pickle.dump(data, BytesIO())", globals=globals(), number=100)
0.000454799999943134
timeit.timeit("pickle.load(BytesIO(seq))", globals=globals(), number=100)
0.0003449999999247666
La sérialisation binaire est habituellement plus rapide dans les langages bas niveau comme C++. La même comparaison pour un langage haut niveau tel que Python n’est pas toujours prévisible. Il est possible d’accélérer un peu les choses.
timeit.timeit(
"pickle.dump(data, BytesIO(), protocol=pickle.HIGHEST_PROTOCOL)",
globals=globals(),
number=100,
)
0.00046400000019275467
Cas des fonctions#
La sérialisation s’applique à des données et non à du code mais le fait de sérialiser des fonctions est tout de même tentant. La sérialisation binaire fonctionne même avec les fonctions.
Binaire#
b'\x80\x04\x95%\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01x\x94K\x05\x8c\x01f\x94\x8c\x08__main__\x94\x8c\x06myfunc\x94\x93\x94u.'
res = pickle.load(BytesIO(buffer.getvalue()))
res
{'x': 5, 'f': <function myfunc at 0x7fc1073d7400>}
6
La sérialisation ne conserve pas le code de la fonction, juste son nom. Cela veut dire que si elle n’est pas disponible lorsqu’elle est appelée, il sera impossible de s’en servir.
del myfunc
try:
pickle.load(BytesIO(buffer.getvalue()))
except Exception as e:
print(e)
Can't get attribute 'myfunc' on <module '__main__'>
Il est possible de contourner l’obstacle en utilisant le module :epkg:`cloudpickle` qui stocke le code de la fonction.
b'\x80\x05\x95\xff\x01\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01x\x94K\x05\x8c\x01f\x94\x8c\x17cloudpickle.cloudpickle\x94\x8c\x0e_make_function\x94\x93\x94(h\x03\x8c\r_builtin_type\x94\x93\x94\x8c\x08CodeType\x94\x85\x94R\x94(K\x01K\x00K\x00K\x01K\x02KCC\x08|\x00d\x01\x17\x00S\x00\x94NK\x01\x86\x94)h\x01\x85\x94\x8cN/home/xadupre/github/teachcompute/_doc/examples/plot_serialisation_examples.py\x94\x8c\x06myfunc\x94M\x97\x01C\x02\x08\x01\x94))t\x94R\x94}\x94(\x8c\x0b__package__\x94\x8c\x00\x94\x8c\x08__name__\x94\x8c\x08__main__\x94uNNNt\x94R\x94\x8c\x1ccloudpickle.cloudpickle_fast\x94\x8c\x12_function_setstate\x94\x93\x94h\x19}\x94}\x94(h\x16h\x0f\x8c\x0c__qualname__\x94h\x0f\x8c\x0f__annotations__\x94}\x94\x8c\x0e__kwdefaults__\x94N\x8c\x0c__defaults__\x94N\x8c\n__module__\x94h\x17\x8c\x07__doc__\x94N\x8c\x0b__closure__\x94N\x8c\x17_cloudpickle_submodules\x94]\x94\x8c\x0b__globals__\x94}\x94u\x86\x94\x86R0u.'
{'x': 5, 'f': <function myfunc at 0x7fc1073d6dd0>}
6
Fonction et JSON#
La sérialisation d’une fonction au format JSON ne fonctionne pas avec le module standard.
Object of type function is not JSON serializable
La sérialisation avec :epkg:`ujson` ne fonctionne pas non plus même si elle ne produit pas toujours d’erreur.
<function myfunc at 0x7fc1073d6f80> is not JSON serializable
''
Cas des itérateurs#
Les itérateurs fonctionnent avec la sérialisation binaire mais ceci implique de stocker l’ensemble que l’itérateur parcourt.
b'\x80\x04\x953\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01x\x94K\x05\x8c\x02it\x94\x8c\x08builtins\x94\x8c\x04iter\x94\x93\x94]\x94(K\x01K\x02e\x85\x94R\x94K\x00bu.'
del ens
res = pickle.load(BytesIO(buffer.getvalue()))
res
{'x': 5, 'it': <list_iterator object at 0x7fc136b97be0>}
list(res["it"])
[1, 2]
list(res["it"])
[]
Cas des générateurs#
Ils ne peuvent être sérialisés car le langage n’a pas accès à l’ensemble des éléments que le générateur parcourt. Il n’y a aucun moyen de sérialiser un générateur mais on peut sérialiser la fonction qui crée le générateur.
cannot pickle 'generator' object
Total running time of the script: (0 minutes 0.045 seconds)