Sérialisation

Links: notebook, html, PDF, python, slides, GitHub

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

from jyquickhelper import add_notebook_menu
add_notebook_menu()

JSON

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

Ecriture

data = {'records': [{'nom': 'Xavier', 'prénom': 'Xavier',
                     'langages':[{'nom':'C++', 'age':40}, {'nom':'Python', 'age': 20}]}]}
from json import dump
from io import StringIO
buffer = StringIO()
res = dump(data, buffer)  # 1
seq = buffer.getvalue()
seq
'{"records": [{"nom": "Xavier", "pr\u00e9nom": "Xavier", "langages": [{"nom": "C++", "age": 40}, {"nom": "Python", "age": 20}]}]}'

Lecture

from json import load
buffer = StringIO(seq)
read = load(buffer)
read
{'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.

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

buffer = StringIO()
try:
    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.m

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

data = A('e')
buffer = StringIO()
try:
    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 JSONEncoder.

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

data = A('e')
buffer = StringIO()
res = dump(data, buffer, cls=MyEncoder)
res = buffer.getvalue()
res
'{"classname": "A", "data": {"att": "e"}}'

Et la relecture avec la classe JSONDecoder.

from json import JSONDecoder

class MyDecoder(JSONDecoder):
        def decode(self, o):
            dec = 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 = load(buffer, cls=MyDecoder)
obj
<__main__.A at 0x24ddb4d27f0>

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}]}]}
%timeit dump(data, StringIO())
28.4 µs ± 2.46 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
from ujson import dump as udump
%timeit udump(data, StringIO())
3.35 µs ± 677 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

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

buffer = StringIO()
dump(data, buffer)
res = buffer.getvalue()
%timeit load(StringIO(res))
8.9 µs ± 1.21 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
from ujson import load as uload
%timeit uload(StringIO(res))
3.25 µs ± 243 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

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

%timeit StringIO(res)
735 ns ± 63.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

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

data = {'records': [{'nom': 'Xavier', 'prénom': 'Xavier',
                     'langages':[{'nom':'C++', 'age':40}, {'nom':'Python', 'age': 20}]}]}
from pickle import dump
from io import BytesIO
buffer = BytesIO()
res = dump(data, buffer)
seq = buffer.getvalue()
seq
b'x80x03}qx00Xx07x00x00x00recordsqx01]qx02}qx03(Xx03x00x00x00nomqx04Xx06x00x00x00Xavierqx05Xx07x00x00x00prxc3xa9nomqx06hx05Xx08x00x00x00langagesqx07]qx08(}qt(hx04Xx03x00x00x00C++qnXx03x00x00x00ageqx0bK(u}qx0c(hx04Xx06x00x00x00Pythonqrhx0bKx14ueuas.'

Lecture

from pickle import load
buffer = BytesIO(seq)
read = 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.

data = A('r')
buffer = BytesIO()
res = dump(data, buffer)
seq = buffer.getvalue()
seq
b'x80x03c__main__nAnqx00)x81qx01}qx02Xx03x00x00x00attqx03Xx01x00x00x00rqx04sb.'
buffer = BytesIO(seq)
read = load(buffer)
read
<__main__.A at 0x24ddb4c36d8>

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 = dump(data, buffer)
seq = buffer.getvalue()
seq
b'x80x03c__main__nBnqx00)x81qx01}qx02(Xx04x00x00x00att1qx03Xx01x00x00x00rqx04Xx04x00x00x00att2qx05hx04ub.'

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 = dump(data, buffer)
seq = buffer.getvalue()
seq
b'x80x03c__main__nBnqx00)x81qx01}qx02Xx03x00x00x00attqx03Xx01x00x00x00rqx04sb.'

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 = load(buffer)
read
<__main__.B at 0x24ddb4b9a90>
read.att1, read.att2
('r', 'r')
data = B('r')
%timeit dump(data, BytesIO())
4.05 µs ± 349 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit load(BytesIO(seq))
5.8 µs ± 874 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

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.

from pickle import HIGHEST_PROTOCOL
%timeit dump(data, BytesIO(), protocol=HIGHEST_PROTOCOL)
4.05 µs ± 294 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

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}

from pickle import dump
from io import BytesIO
buffer = BytesIO()
res = dump(data, buffer)
buffer.getvalue()
b'x80x03}qx00(Xx01x00x00x00xqx01Kx05Xx01x00x00x00fqx02c__main__nmyfuncnqx03u.'
from pickle import load
res = load(BytesIO(buffer.getvalue()))
res
{'x': 5, 'f': <function __main__.myfunc(x)>}
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é, il sera impossible de s’en servir.

del myfunc

from pickle import load
try:
    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 cloudpicke qui stocke le code de la fonction.

def myfunc(x):
    return x + 1

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

from cloudpickle import dump
from io import BytesIO
buffer = BytesIO()
res = dump(data, buffer)
buffer.getvalue()
b'x80x04x95x9bx01x00x00x00x00x00x00}x94(x8cx01xx94Kx05x8cx01fx94x8cx17cloudpickle.cloudpicklex94x8cx0e_fill_functionx94x93x94(hx03x8cx0f_make_skel_funcx94x93x94hx03x8cr_builtin_typex94x93x94x8cx08CodeTypex94x85x94Rx94(Kx01Kx00Kx01Kx02KCCx08|x00dx01x17x00Sx00x94NKx01x86x94)hx01x85x94x8cx1f<ipython-input-32-12529e4b8824>x94x8cx06myfuncx94Kx01Cx02x00x01x94))tx94Rx94Jxffxffxffxff}x94(x8cx0b__package__x94Nx8cx08__name__x94x8cx08__main__x94ux87x94Rx94}x94(x8cx07globalsx94}x94x8cx08defaultsx94Nx8cx04dictx94}x94x8cx0eclosure_valuesx94Nx8cx06modulex94hx18x8cx04namex94hx11x8cx03docx94Nx8cx17_cloudpickle_submodulesx94]x94x8cx0bannotationsx94}x94x8cx08qualnamex94hx11x8cnkwdefaultsx94NutRu.'
del myfunc

from cloudpickle import load
res = load(BytesIO(buffer.getvalue()))
res
{'x': 5, 'f': <function __main__.myfunc(x)>}
res['f'](res['x'])
6

JSON

La sérialisation au format JSON ne fonctionne pas avec le module standard.

from json import dump
from io import StringIO
buffer = StringIO()
try:
    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.

from ujson import dump
from io import StringIO
buffer = StringIO()
try:
    res = dump(data, buffer)  # 3
except TypeError as e:
    print(e)
buffer.getvalue()
'{"x":5,"f":{}}'

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

from pickle import dump
from io import BytesIO
buffer = BytesIO()
res = dump(data, buffer)  # 4
buffer.getvalue()
b'x80x03}qx00(Xx01x00x00x00xqx01Kx05Xx02x00x00x00itqx02cbuiltinsniternqx03]qx04(Kx01Kx02ex85qx05Rqx06Kx00bu.'
del ens
from pickle import load
res = load(BytesIO(buffer.getvalue()))
res
{'x': 5, 'it': <list_iterator at 0x24ddb515d30>}
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()}

from pickle import dump
from io import BytesIO
buffer = BytesIO()
try:
    dump(data, buffer)
except Exception as e:
    print(e)
can't pickle generator objects