.. _serialisationexamplesrst: ============= Sérialisation ============= .. only:: html **Links:** :download:`notebook `, :downloadlink:`html `, :download:`PDF `, :download:`python `, :downloadlink:`slides `, :githublink:`GitHub|_doc/notebooks/python/serialisation_examples.ipynb|*` Le notebook explore différentes façons de sérialiser des données et leurs limites. .. code:: ipython3 from jyquickhelper import add_notebook_menu add_notebook_menu() .. contents:: :local: JSON ---- Le format `JSON `__ est le format le plus utilisé sur internet notemmant via les `API REST `__. Ecriture ~~~~~~~~ .. code:: ipython3 data = {'records': [{'nom': 'Xavier', 'prénom': 'Xavier', 'langages':[{'nom':'C++', 'age':40}, {'nom':'Python', 'age': 20}]}]} .. code:: ipython3 from json import dump from io import StringIO buffer = StringIO() res = dump(data, buffer) # 1 seq = buffer.getvalue() seq .. parsed-literal:: '{"records": [{"nom": "Xavier", "pr\\u00e9nom": "Xavier", "langages": [{"nom": "C++", "age": 40}, {"nom": "Python", "age": 20}]}]}' Lecture ~~~~~~~ .. code:: ipython3 from json import load buffer = StringIO(seq) read = load(buffer) read .. parsed-literal:: {'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. .. code:: ipython3 import numpy data = {'mat': numpy.array([0, 1])} buffer = StringIO() try: dump(data, buffer) except Exception as e: print(e) .. parsed-literal:: Object of type ndarray is not JSON serializable Les classes ne sont pas sérialisables non plus facilement.m .. code:: ipython3 class A: def __init__(self, att): self.att = att data = A('e') buffer = StringIO() try: dump(data, buffer) except Exception as e: print(e) .. parsed-literal:: 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 `__. .. code:: ipython3 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 .. parsed-literal:: '{"classname": "A", "data": {"att": "e"}}' Et la relecture avec la classe `JSONDecoder `__. .. code:: ipython3 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 .. parsed-literal:: <__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 `__. .. code:: ipython3 data = {'records': [{'nom': 'Xavier', 'prénom': 'Xavier', 'langages':[{'nom':'C++', 'age':40}, {'nom':'Python', 'age': 20}]}]} .. code:: ipython3 %timeit dump(data, StringIO()) .. parsed-literal:: 28.4 µs ± 2.46 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) .. code:: ipython3 from ujson import dump as udump %timeit udump(data, StringIO()) .. parsed-literal:: 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. .. code:: ipython3 buffer = StringIO() dump(data, buffer) res = buffer.getvalue() %timeit load(StringIO(res)) .. parsed-literal:: 8.9 µs ± 1.21 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each) .. code:: ipython3 from ujson import load as uload %timeit uload(StringIO(res)) .. parsed-literal:: 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. .. code:: ipython3 %timeit StringIO(res) .. parsed-literal:: 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 ~~~~~~~~ .. code:: ipython3 data = {'records': [{'nom': 'Xavier', 'prénom': 'Xavier', 'langages':[{'nom':'C++', 'age':40}, {'nom':'Python', 'age': 20}]}]} .. code:: ipython3 from pickle import dump from io import BytesIO buffer = BytesIO() res = dump(data, buffer) seq = buffer.getvalue() seq .. parsed-literal:: b'\x80\x03}q\x00X\x07\x00\x00\x00recordsq\x01]q\x02}q\x03(X\x03\x00\x00\x00nomq\x04X\x06\x00\x00\x00Xavierq\x05X\x07\x00\x00\x00pr\xc3\xa9nomq\x06h\x05X\x08\x00\x00\x00langagesq\x07]q\x08(}q\t(h\x04X\x03\x00\x00\x00C++q\nX\x03\x00\x00\x00ageq\x0bK(u}q\x0c(h\x04X\x06\x00\x00\x00Pythonq\rh\x0bK\x14ueuas.' Lecture ~~~~~~~ .. code:: ipython3 from pickle import load buffer = BytesIO(seq) read = load(buffer) read .. parsed-literal:: {'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. .. code:: ipython3 data = A('r') buffer = BytesIO() res = dump(data, buffer) seq = buffer.getvalue() seq .. parsed-literal:: b'\x80\x03c__main__\nA\nq\x00)\x81q\x01}q\x02X\x03\x00\x00\x00attq\x03X\x01\x00\x00\x00rq\x04sb.' .. code:: ipython3 buffer = BytesIO(seq) read = load(buffer) read .. parsed-literal:: <__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. .. code:: ipython3 class B: def __init__(self, att): self.att1 = att self.att2 = att .. code:: ipython3 data = B('r') buffer = BytesIO() res = dump(data, buffer) seq = buffer.getvalue() seq .. parsed-literal:: b'\x80\x03c__main__\nB\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00att1q\x03X\x01\x00\x00\x00rq\x04X\x04\x00\x00\x00att2q\x05h\x04ub.' Evitons maintenant de stocker deux fois le même attribut. .. code:: ipython3 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 .. parsed-literal:: b'\x80\x03c__main__\nB\nq\x00)\x81q\x01}q\x02X\x03\x00\x00\x00attq\x03X\x01\x00\x00\x00rq\x04sb.' C’est plus court mais il faut inclure maintenant la relecture. .. code:: ipython3 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 .. parsed-literal:: <__main__.B at 0x24ddb4b9a90> .. code:: ipython3 read.att1, read.att2 .. parsed-literal:: ('r', 'r') .. code:: ipython3 data = B('r') %timeit dump(data, BytesIO()) .. parsed-literal:: 4.05 µs ± 349 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) .. code:: ipython3 %timeit load(BytesIO(seq)) .. parsed-literal:: 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. .. code:: ipython3 from pickle import HIGHEST_PROTOCOL %timeit dump(data, BytesIO(), protocol=HIGHEST_PROTOCOL) .. parsed-literal:: 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 ~~~~~~~ .. code:: ipython3 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() .. parsed-literal:: b'\x80\x03}q\x00(X\x01\x00\x00\x00xq\x01K\x05X\x01\x00\x00\x00fq\x02c__main__\nmyfunc\nq\x03u.' .. code:: ipython3 from pickle import load res = load(BytesIO(buffer.getvalue())) res .. parsed-literal:: {'x': 5, 'f': } .. code:: ipython3 res['f'](res['x']) .. parsed-literal:: 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. .. code:: ipython3 del myfunc from pickle import load try: load(BytesIO(buffer.getvalue())) except Exception as e: print(e) .. parsed-literal:: Can't get attribute 'myfunc' on Il est possible de contourner l’obstacle en utilisant le module `cloudpicke `__ qui stocke le code de la fonction. .. code:: ipython3 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() .. parsed-literal:: b'\x80\x04\x95\x9b\x01\x00\x00\x00\x00\x00\x00}\x94(\x8c\x01x\x94K\x05\x8c\x01f\x94\x8c\x17cloudpickle.cloudpickle\x94\x8c\x0e_fill_function\x94\x93\x94(h\x03\x8c\x0f_make_skel_func\x94\x93\x94h\x03\x8c\r_builtin_type\x94\x93\x94\x8c\x08CodeType\x94\x85\x94R\x94(K\x01K\x00K\x01K\x02KCC\x08|\x00d\x01\x17\x00S\x00\x94NK\x01\x86\x94)h\x01\x85\x94\x8c\x1f\x94\x8c\x06myfunc\x94K\x01C\x02\x00\x01\x94))t\x94R\x94J\xff\xff\xff\xff}\x94(\x8c\x0b__package__\x94N\x8c\x08__name__\x94\x8c\x08__main__\x94u\x87\x94R\x94}\x94(\x8c\x07globals\x94}\x94\x8c\x08defaults\x94N\x8c\x04dict\x94}\x94\x8c\x0eclosure_values\x94N\x8c\x06module\x94h\x18\x8c\x04name\x94h\x11\x8c\x03doc\x94N\x8c\x17_cloudpickle_submodules\x94]\x94\x8c\x0bannotations\x94}\x94\x8c\x08qualname\x94h\x11\x8c\nkwdefaults\x94NutRu.' .. code:: ipython3 del myfunc from cloudpickle import load res = load(BytesIO(buffer.getvalue())) res .. parsed-literal:: {'x': 5, 'f': } .. code:: ipython3 res['f'](res['x']) .. parsed-literal:: 6 JSON ~~~~ La sérialisation au format JSON ne fonctionne pas avec le module standard. .. code:: ipython3 from json import dump from io import StringIO buffer = StringIO() try: dump(data, buffer) # 2 except Exception as e: print(e) .. parsed-literal:: 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. .. code:: ipython3 from ujson import dump from io import StringIO buffer = StringIO() try: res = dump(data, buffer) # 3 except TypeError as e: print(e) buffer.getvalue() .. parsed-literal:: '{"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. .. code:: ipython3 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() .. parsed-literal:: b'\x80\x03}q\x00(X\x01\x00\x00\x00xq\x01K\x05X\x02\x00\x00\x00itq\x02cbuiltins\niter\nq\x03]q\x04(K\x01K\x02e\x85q\x05Rq\x06K\x00bu.' .. code:: ipython3 del ens from pickle import load res = load(BytesIO(buffer.getvalue())) res .. parsed-literal:: {'x': 5, 'it': } .. code:: ipython3 list(res["it"]) .. parsed-literal:: [1, 2] .. code:: ipython3 list(res["it"]) .. parsed-literal:: [] 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. .. code:: ipython3 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) .. parsed-literal:: can't pickle generator objects