Sérialisation

Le notebook explore différentes façons de sérialiser des données et leurs limites.

JSON

Le format JSON est le format le plus utilisé sur internet notemmant via les API REST.

Ecriture (json)

from io import StringIO, BytesIO
import timeit
import json
import numpy
import ujson
import cloudpickle
import pickle
import matplotlib.pyplot as plt
import pandas


data = {
    "records": [
        {
            "nom": "Xavier",
            "prénom": "Xavier",
            "langages": [{"nom": "C++", "age": 40}, {"nom": "Python", "age": 20}],
        }
    ]
}
buffer = StringIO()
res = json.dump(data, buffer)  # 1
seq = buffer.getvalue()
seq
'{"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.

data = {"mat": numpy.array([0, 1])}

buffer = StringIO()
try:
    json.dump(data, buffer)
except Exception as e:
    print(e)
Object of type ndarray is not JSON serializable

Les classes ne sont pas sérialisables non plus facilement.

class A:
    def __init__(self, att):
        self.att = att


data = A("e")
buffer = StringIO()
try:
    json.dump(data, buffer)
except Exception as e:
    print(e)
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.

class MyEncoder(json.JSONEncoder):
    def default(self, o):
        return {"classname": o.__class__.__name__, "data": o.__dict__}


data = A("e")
buffer = StringIO()
res = json.dump(data, buffer, cls=MyEncoder)
res = buffer.getvalue()
res
'{"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 0x7f5068297880>

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 ujson.

data = {
    "records": [
        {
            "nom": "Xavier",
            "prénom": "Xavier",
            "langages": [{"nom": "C++", "age": 40}, {"nom": "Python", "age": 20}],
        }
    ]
}
data_time = []
expression = "json.dump(data, StringIO())"
d = timeit.timeit(expression, globals=globals(), number=100)
data_time.append(dict(expression=expression, time=d))
d
0.003245500000048196
expression = "ujson.dump(data, StringIO())"
d = timeit.timeit(expression, globals=globals(), number=100)
data_time.append(dict(expression=expression, time=d))
d
0.00038890000041647

Ces deux lignes mesures l’écriture au format JSON mais il faut aussi mesurer la lecture.

buffer = StringIO()
ujson.dump(data, buffer)
res = buffer.getvalue()

expression = "json.load(StringIO(res))"
d = timeit.timeit(expression, globals=globals(), number=100)
data_time.append(dict(expression=expression, time=d))
d
0.0006450999999287887
expression = "ujson.load(StringIO(res))"
d = timeit.timeit(expression, globals=globals(), number=100)
data_time.append(dict(expression=expression, time=d))
d
0.0004648999997698411

On enlève le temps passé dans la creation du buffer.

expression = "StringIO(res)"
d = timeit.timeit(expression, globals=globals(), number=100)
data_time.append(dict(expression=expression, time=d))
d
4.2499999835854396e-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)

{'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.

data = A("r")
buffer = BytesIO()
res = pickle.dump(data, buffer)
seq = buffer.getvalue()
seq
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.'
<__main__.A object at 0x7f50684cf8e0>

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
data = B("r")
buffer = BytesIO()
res = pickle.dump(data, buffer)
seq = buffer.getvalue()
seq
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.

class B:
    def __init__(self, att):
        self.att1 = att
        self.att2 = att

    def __getstate__(self):
        return dict(att=self.att1)


data = B("r")
buffer = BytesIO()
res = pickle.dump(data, buffer)
seq = buffer.getvalue()
seq
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 0x7f504b672560>
('r', 'r')
data = B("r")
expression = "pickle.dump(data, BytesIO())"
d = timeit.timeit(expression, globals=globals(), number=100)
data_time.append(dict(expression=expression, time=d))
d
0.0004768000003423367
expression = "pickle.load(BytesIO(seq))"
d = timeit.timeit(expression, globals=globals(), number=100)
data_time.append(dict(expression=expression, time=d))
d
0.00031349999972007936

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.

expression = "pickle.dump(data, BytesIO(), protocol=pickle.HIGHEST_PROTOCOL)"
d = timeit.timeit(expression, globals=globals(), number=100)
data_time.append(dict(expression=expression, time=d))
d
0.00044460000026447233

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

def myfunc(x):
    return x + 1


data = {"x": 5, "f": myfunc}


buffer = BytesIO()
res = pickle.dump(data, buffer)
buffer.getvalue()
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 0x7f504b542ef0>}
res["f"](res["x"])
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 cloudpickle qui stocke le code de la fonction.

def myfunc(x):
    return x + 1


data = {"x": 5, "f": myfunc}


buffer = BytesIO()
res = cloudpickle.dump(data, buffer)
buffer.getvalue()
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\xa7\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.'
del myfunc


res = cloudpickle.load(BytesIO(buffer.getvalue()))
res
{'x': 5, 'f': <function myfunc at 0x7f504b542a70>}
res["f"](res["x"])
6

Fonction et JSON

La sérialisation d’une fonction au format JSON ne fonctionne pas avec le module standard.

buffer = StringIO()
try:
    json.dump(data, buffer)  # 2
except Exception as e:
    print(e)
Object of type function is not JSON serializable

La sérialisation avec ujson ne fonctionne pas non plus même si elle ne produit pas toujours d’erreur.

buffer = StringIO()
try:
    res = ujson.dump(data, buffer)  # 3
except TypeError as e:
    print(e)
buffer.getvalue()
<function myfunc at 0x7f504b542c20> 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.

ens = [1, 2]

data = {"x": 5, "it": iter(ens)}


buffer = BytesIO()
res = pickle.dump(data, buffer)  # 4
buffer.getvalue()
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 0x7f504b671e70>}
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.

def ensgen():
    yield 1
    yield 2


data = {"x": 5, "it": ensgen()}


buffer = BytesIO()
try:
    pickle.dump(data, buffer)
except Exception as e:
    print(e)
cannot pickle 'generator' object

Summary

                                          expression      time
0                        json.dump(data, StringIO())  0.003246
1                       ujson.dump(data, StringIO())  0.000389
2                           json.load(StringIO(res))  0.000645
3                          ujson.load(StringIO(res))  0.000465
4                                      StringIO(res)  0.000042
5                       pickle.dump(data, BytesIO())  0.000477
6                          pickle.load(BytesIO(seq))  0.000313
7  pickle.dump(data, BytesIO(), protocol=pickle.H...  0.000445
fig, ax = plt.subplots(1, 2, figsize=(10, 4), sharey=True)
df.set_index("expression").plot.barh(ax=ax[0])
df.loc[0, "time"] = numpy.nan
df.set_index("expression").plot.barh(ax=ax[1])
ax[0].set_title("Time")
ax[1].set_title("Time without `json.dump`")
fig.tight_layout()
fig.savefig("plot_serialisation_examples.png")
Time, Time without `json.dump`

Total running time of the script: (0 minutes 0.496 seconds)

Gallery generated by Sphinx-Gallery