Skip to content

Commit

Permalink
add content on serialisation #7
Browse files Browse the repository at this point in the history
  • Loading branch information
sdpython committed Apr 20, 2018
1 parent 83dd2ca commit 0cd0743
Show file tree
Hide file tree
Showing 10 changed files with 1,600 additions and 10 deletions.
1,277 changes: 1,277 additions & 0 deletions _doc/notebooks/python/serialisation_examples.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions _doc/sphinxdoc/source/c_io/index.rst
Expand Up @@ -7,3 +7,4 @@ Entrées Sorties
:maxdepth: 2

files
serialization
304 changes: 304 additions & 0 deletions _doc/sphinxdoc/source/c_io/serialization.rst
@@ -0,0 +1,304 @@

.. _chap_serialization:

=============
Sérialisation
=============

.. index:: sérialisation

La `sérialisation <https://fr.wikipedia.org/wiki/S%C3%A9rialisation>`_ est
un besoin fréquent depuis l'avènement d'internet. Une appplication
web est souvent un assemblage de services indépendant qui s'échangent des
informations complexes. Un service doit transmettre des données
à un autre. L'opération est simple lorsqu'il s'agit de transmettre
un nombre réel, un entier, une chaîne de caractère. C'est déjà
un peu plus compliqué lorsque les données à transmettre sont
constituées de listes et de dictionnaires. Elle est complexe
lorsque ce sont des instances de classes définies par le programme
lui-même. Or les services s'échangent uniquement des octets
(ou *byte* en anglais). Il faut donc convertir des informations
complexes en une séquence d'octets puis effectuer la conversion
inverse. Ces opérations sont regroupées sous le terme
de *sérialisation*.

.. contents::
:local:
:depth: 2

JSON
====

Un des premiers utilisés dans le monde internet est le format
`XML <https://fr.wikipedia.org/wiki/Extensible_Markup_Language>`_.
Le langage `HTML <https://fr.wikipedia.org/wiki/Hypertext_Markup_Language>`_
suit la même logique que le format *XML*.
Il peut représenter un assemblage de liste et de dictionnaires
qui contiennent des nombres et des chaînes de caractères.

::

<xml>
<personne> <!-- dictionnaire -->
<nom>Dupré</nom> <!-- str -->
<prenom>Xavier</prenom> <!-- str -->
<langages> <!-- liste -->
<lang> <!-- dictionnaire -->
<nom>C++</nom> <!-- str -->
<vitesse>rapide</nom> <!-- str -->
<age>40</age> <!-- int -->
</lang>
<lang> <!-- dictionnaire -->
<nom>python</nom> <!-- str -->
<vitesse>lente</nom> <!-- str -->
<age>20</age> <!-- int -->
</lang>
</langages>
</personnes>
</xml>

La sérialisation passe par une étape intermédiaire qui revient à
convertir tout type de variables en une séquence de listes et
de dictionnaires puis à la convertir sous la forme d'une séquence d'octets.

.. blockdiag::

blockdiag {
A [label = "variables"];
B [label = "dictionnaires, listes"];
C [label = "séquence d'octets"];
A -> B -> C;
}

Le format `XML <https://fr.wikipedia.org/wiki/Extensible_Markup_Language>`_
intervient lors de la seconde étape. Il est assez *verbeux*. La séquence
d'octets ou plutôt de caractères qu'il produit est assez longue même une fois
les espaces enlevés. On lui préfère le format :epkg:`JSON`
qui est très utilisé pour communiquer entre les services via
des `API REST <https://fr.wikipedia.org/wiki/Representational_state_transfer>`_
car il est à la fois concis et lisible.

.. runpython::
:showcode:

from json import dump
from io import StringIO

data = {'records': [{'nom': 'Xavier', 'prénom': 'Xavier',
'langages':[{'nom':'C++', 'age':40},
{'nom':'Python', 'age': 20}]}]}

buffer = StringIO()
res = dump(data, buffer)
seq = buffer.getvalue()
print(seq)

Le format :epkg:`JSON` est très proche de la syntaxe du langage :epkg:`Python`.
Le résultat apparaît sous la forme d'une chaîne de caractère et est
facile à transmettre à n'importe quelle autre application.
La conversion inverse s'effectue comme suit.

.. runpython::
:showcode:

from json import load
from io import StringIO

seq = '{"records": [{"nom": "Xavier", "pr\\u00e9nom": "Xavier", ' + \
"langages": [{"nom": "C++", "age": 40}, {"nom": "Python", "age": 20}]}]}'

buffer = StringIO(seq)
read = load(buffer)
read

Tous les types ne sont pas sérialisables même si ceux-ci
sont fréquemment utilisés :

.. runpython::
:showcode:

from json import dump
from io import StringIO
import numpy

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

buffer = StringIO()
try:
dump(data, buffer)
except Exception as e:
print(e)

Les classes définies dans le programme ne sont pas non
plus sérialisables par défaut.

.. runpython::
:showcode:

from json import dump
from io import StringIO

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

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

data = A('e')
buffer = StringIO()
try:
dump(data, buffer, cls=MyEncoder)
except Exception as e:
print(e)

On peut néanmoins contourner l'obstacle en indiquant à la fonction
`dump <https://docs.python.org/3/library/json.html#json.dump>`_
comment convertir l'instance en un assemblage de listes et de dictionnaires
avec la classe `JSONEncoder <https://docs.python.org/3/library/json.html#json.JSONEncoder>`_.

.. runpython::
:showcode:

from json import JSONEncoder
from json import dump
from io import StringIO

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

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

Et la relecture s'effectue comme suit :

.. runpython::
:showcode:

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

res = '{"classname": "A", "data": {"att": "e"}}'
buffer = StringIO(res)
obj = load(buffer, cls=MyDecoder)
obj

Il existe des alternatives plus rapides au module
`json <https://docs.python.org/3/library/json.html>`_
come le module :epkg:`ultrajson`.

Binaire
=======

Le format *binaire* n'est pas un format à proprement parler.
Le problème principale lié à l'utilisation d'un format comme
:epkg:`JSON` est le temps passé à convertir les informations
numériques en texte et réciproquement. Il est plus efficace
de conserver cette information dans un format plus proche de
celui de la mémoire. Cette remarque est aussi vrai pour tous
les autre types. Lorsque la sérialisation est nécessaire au
sein d'une même application, il est nettement préférable de
choisir ce type de format même s'il est illisible pour un autre
langage voire même pour un autre programme. Le code ressemble
beaucoup à ce qui a été écrit pour le format :epkg:`JSON`.

.. runpython::
:showcode:

from pickle import dump
from io import BytesIO

data = {'records': [{'nom': 'Xavier', 'prénom': 'Xavier',
'langages':[{'nom':'C++', 'age':40},
{'nom':'Python', 'age': 20}]}]}

buffer = BytesIO()
res = dump(data, buffer)
seq = buffer.getvalue()
print(seq)

La relecture s'effectue tout aussi simplement.

.. runpython::
:showcode:

from pickle import load, dump
from io import BytesIO

data = {'records': [{'nom': 'Xavier', 'prénom': 'Xavier',
'langages':[{'nom':'C++', 'age':40},
{'nom':'Python', 'age': 20}]}]}

# écriture
buffer = BytesIO()
res = dump(data, buffer)
seq = buffer.getvalue()

# lecture
buffer = BytesIO(seq)
read = load(buffer)
print(read)

Certaines configurations impossible, un peu comme pour
le format :epkg:`JSON`, de modifier les éléments sérialisés
et restaurés. Cela se fait grâce aux fonctions
`__getstate__ <https://docs.python.org/3/library/pickle.html#object.__getstate__>`_ et
`__setstate__ <https://docs.python.org/3/library/pickle.html#object.__setstate__>`_.
Dans l'exemple suivant, la classe copie un attribut deux fois.
Pour réduire la taille de l'objet une fois sérialisé, on en stocke qu'un seul.

.. runpython::
:showcode:

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

def __getstate__(self):
# On indique les attributs à converser.
return dict(att=self.att1)

def __setstate__(self, state):
# On restaure l'objet à partir de ce qui a été sérialisé.
setattr(self, 'att1', state["att"])
setattr(self, 'att2', state["att"])

data = B('r')
buffer = BytesIO()
res = dump(data, buffer)
seq = buffer.getvalue()

buffer = BytesIO(seq)
read = load(buffer)
print(read.att1, read.att2)

Sérialiser autre chose que des données
======================================

D'une manière générale, il vaut mieux éviter de sérialiser
les fonctions, itérateurs ou générateurs. Il est souvent impossible
des les sérialiser proprement sans ajouter du code spécifiquement
développés pour cela. Le notebook suivant étudie certains cas
où cela est néanmoins possible sans trop de développement.

.. toctree::
:maxdepth:

../notebooks/serialisation_examples
10 changes: 6 additions & 4 deletions _doc/sphinxdoc/source/conf.py
@@ -1,4 +1,4 @@
#-*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
import sys
import os
import datetime
Expand Down Expand Up @@ -29,6 +29,7 @@
title="Programmation avec le langage Python", book=True, nblayout='table')

blog_root = "http://www.xavierdupre.fr/app/teachpyx/helpsphinx/"
extensions.append("sphinxcontrib-blockdiag")

html_context = {
'css_files': get_default_stylesheet() + ['_static/my-styles.css'],
Expand All @@ -42,8 +43,6 @@
'navbar_site_name': "Site",
'navbar_links': [
("XD", "http://www.xavierdupre.fr", True),
("blog", "blog/main_0000.html", True),
("index", "genindex"),
],
'navbar_sidebarrel': True,
'navbar_pagenav': True,
Expand Down Expand Up @@ -110,7 +109,7 @@
\\newcommand{\\sac}[0]{|}
\\newcommand{\\abs}[1]{\\left|#1\\right|}
"""
#\\usepackage{eepic}
# \\usepackage{eepic}

imgmath_latex_preamble = preamble + custom_preamble
latex_elements['preamble'] = preamble + custom_preamble
Expand All @@ -119,7 +118,10 @@
epkg_dictionary["C++"] = 'https://fr.wikipedia.org/wiki/C%2B%2B'
epkg_dictionary["format"] = 'https://docs.python.org/3/library/stdtypes.html#str.format'
epkg_dictionary["joblib"] = 'https://pythonhosted.org/joblib/'
epkg_dictionary["JSON"] = 'https://fr.wikipedia.org/wiki/JavaScript_Object_Notation'
epkg_dictionary["nan"] = 'https://docs.scipy.org/doc/numpy/reference/generated/numpy.isnan.html'
epkg_dictionary["NaN"] = 'https://docs.scipy.org/doc/numpy/reference/generated/numpy.isnan.html'
epkg_dictionary["OpenMP"] = 'https://fr.wikipedia.org/wiki/OpenMP'
epkg_dictionary["pyformat"] = 'https://pyformat.info/'
epkg_dictionary["ujson"] = 'https://github.com/esnme/ultrajson'
epkg_dictionary["ultrajson"] = 'https://github.com/esnme/ultrajson'
2 changes: 1 addition & 1 deletion _unittests/ut_documentation/test_run_notebooks_pandas.py
@@ -1,4 +1,4 @@
#-*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
"""
@brief test log(time=33s)
"""
Expand Down
2 changes: 1 addition & 1 deletion _unittests/ut_documentation/test_run_notebooks_python.py
@@ -1,4 +1,4 @@
#-*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
"""
@brief test log(time=33s)
"""
Expand Down
6 changes: 4 additions & 2 deletions _unittests/ut_examples/test_pandasex.py
Expand Up @@ -63,7 +63,8 @@ def test_pandasex(self):

for ty in types:
data = [{"this": "cst", "type": "tt1=" + str(ty[0]), "value": ty[1]},
{"this": "cst", "type": "tt2=" + str(ty[0]), "value": ty[1]},
{"this": "cst", "type": "tt2=" +
str(ty[0]), "value": ty[1]},
{"this": "cst", "type": "row_for_nan"}]
df = pandas.DataFrame(data)
gr = pandas_groupby_nan(df, "value")
Expand All @@ -74,7 +75,8 @@ def test_pandasex(self):

for ty in types:
data = [{"this": "cst", "type": "tt1=" + str(ty[0]), "value": ty[1]},
{"this": "cst", "type": "tt2=" + str(ty[0]), "value": ty[1]},
{"this": "cst", "type": "tt2=" +
str(ty[0]), "value": ty[1]},
{"this": "cst", "type": "row_for_nan"}]
df = pandas.DataFrame(data)
try:
Expand Down
3 changes: 2 additions & 1 deletion appveyor.yml
Expand Up @@ -19,7 +19,8 @@ install:
- "%PYTHON%\\python -m pip install --upgrade pip"
- "%PYTHON%\\Scripts\\pip install pymyinstall"
- "%PYTHON%\\Scripts\\pymy_install3 --set=pyquickhelper"
- "%PYTHON%\\Scripts\\pip install pycodestyle prompt_toolkit pyquickhelper jyquickhelper"
- "%PYTHON%\\Scripts\\pip install pycodestyle prompt_toolkit pyquickhelper jyquickhelper sphinxcontrib-blockdiag cloudpickle"
- "%PYTHON%\\Scripts\\pymy_install3 ujson"
- "%PYTHON%\\Scripts\\pymy_install3 --task=tool --source=zip graphviz"
- "set PATH=%PATH%;C:\\projects\\teachpyx\\build\\update_modules\\Graphviz\\bin"
build: off
Expand Down

0 comments on commit 0cd0743

Please sign in to comment.