Sérialisation et désérialisation d'objets Python Partie 1

La sérialisation et la désérialisation des objets Python constituent un aspect important de tout programme non trivial. Si vous enregistrez quelque chose dans un fichier, si vous lisez un fichier de configuration ou si vous répondez à une requête HTTP, vous effectuez la sérialisation et la désérialisation des objets.. 

En un sens, la sérialisation et la désérialisation sont les choses les plus ennuyeuses au monde. Qui se soucie de tous les formats et protocoles? Vous voulez juste persister ou diffuser des objets Python et les récupérer plus tard intacts. 

C'est une manière très saine de regarder le monde au niveau conceptuel. Mais, au niveau pragmatique, le schéma, le format ou le protocole de sérialisation que vous choisissez peut déterminer la vitesse d'exécution de votre programme, son niveau de sécurité, votre degré de liberté pour maintenir votre état et votre capacité d'interopération avec d'autres systèmes. 

La raison pour laquelle il y a tant d'options est que différentes circonstances appellent des solutions différentes. Il n'y a pas de "taille unique". Dans ce didacticiel en deux parties, je passerai en revue les avantages et les inconvénients des schémas de sérialisation et de désérialisation les plus réussis, leur montrer comment les utiliser et leur indiquer comment choisir entre ceux-ci lorsqu'ils sont confrontés à un cas d'utilisation spécifique..

Exemple en cours d'exécution

Dans les sections suivantes, je vais sérialiser et désérialiser les mêmes graphiques d'objet Python à l'aide de différents sérialiseurs. Pour éviter les répétitions, je vais définir ces graphiques d'objets ici.

Graphique d'objet simple

Le graphe d'objet simple est un dictionnaire contenant une liste d'entiers, une chaîne, un float, un booléen et un.

simple = dict (int_list = [1, 2, 3], text = "chaîne", nombre = 3.44, boolean = True, none = None) 

Graphique d'objet complexe

Le graphe d'objet complexe est aussi un dictionnaire, mais il contient un date / heure objet et instance de classe définie par l'utilisateur qui a un auto.simple attribut, qui est défini sur le graphe d'objet simple.

depuis date-heure importation date-heure classe A (objet): def __init __ (self, simple): self.simple = simple def __eq __ (self, autre): si hasattr (other, 'simple'): return False return self.simple == other.simple def __ne __ (self, other): sinon hasattr (other, 'simple'): return True return self.simple! = other.simple complex = dict (a = A (simple), when = datetime (2016, 3, 7))

Cornichon

Pickle est un aliment de base. C'est un format de sérialisation d'objet Python natif. L'interface pickle propose quatre méthodes: dump, dumps, load et load. le déverser() La méthode est sérialisée dans un fichier ouvert (objet de type fichier). le décharges () La méthode se sérialise à une chaîne. le charge() La méthode se désérialise à partir d'un objet de type fichier ouvert. le charges() la méthode se désérialise à partir d'une chaîne.

Pickle prend en charge par défaut un protocole textuel, mais dispose également d'un protocole binaire, plus efficace, mais non lisible par l'homme (utile lors du débogage)..

Voici comment choisir un graphe d'objet Python dans une chaîne et dans un fichier utilisant les deux protocoles.

importer cPickle en tant que pickle pickle.dumps (simple) "(dp1 \ nS'text '\ np2 \ nS'string' \ np3 \ nsS'none" \ np4 \ nNsS'boolean '\ np5 \ nI01 \ nsS'number "\ np6 \ nF3.4399999999999999 \ nsS'int_list '\ np7 \ n (lp8 \ nI1 \ naI2 \ naI3 \ nas. "pickle.dumps (simple, protocole = pickle.HIGHEST_PROTOCOL)' \ x80 \ x02 q \ x01 (U \ x04text) \ x02U \ x06stringq \ x03U \ x04noneq \ x04NU \ x07boolean \ x88U \ x06numberq \ x05G @ \ x0b \ x1e \ xb8Q \ xeb \ x85U \ x05 \ x05 \ x05 \ x05 \ x0 \ x0 \ x0 \ x0 \ x0 \ x0 \ n \ x0 \ u0026quot;

La représentation binaire peut sembler plus grande, mais c'est une illusion due à sa présentation. Lors du basculement dans un fichier, le protocole textuel est de 130 octets, tandis que le protocole binaire n’est que de 85 octets..

pickle.dump (simple, ouvert ('simple1.pkl', 'w')) pickle.dump (simple, ouvert ('simple2.pkl', 'wb'), protocole = pickle.HIGHEST_PROTOCOL) ls -la sim *. * -rw-r - r-- 1 gigi de personnel 130 9 mars 02:42 simple1.pkl -rw-r - r-- 1 gigi de personnel 85 mars 9 02:43 simple2.pkl

Décompresser d'une chaîne est aussi simple que:

x = pickle.loads ("(dp1 \ nS'text '\ np2 \ nS'string' \ np3 \ nsS'none '\ np4 \ nNsS'boolean' \ np5 \ nI01 \ nsS'number" \ np6 \ nF3.4399999999999999 \ nsS'int_list '\ np7 \ n (lp8 \ nI1 \ naI2 \ naI3 \ nas. ") assert x == simple x = pickle.loads (' \ x80 \ x02 q \ x01 (U \ x04textq \ x02U \ x02U \ x06stringq \ x03U \ x04noneq \ x04NU \ x07boolean \ x88U \ x06numberq \ x05G @ \ x0b \ x85 \ x1e \ xb8Q \ x85 \ x06int_list] q \ x06 (K \ x01K \ x02 \ x03 \ x03 \ x0).

Notez que pickle peut comprendre le protocole automatiquement. Il n'y a pas besoin de spécifier un protocole même pour le binaire.

Décompresser dans un fichier est tout aussi simple. Vous avez juste besoin de fournir un fichier ouvert.

x = pickle.load (open ('simple1.pkl')) assert x == simple x = pickle.load (open ('simple2.pkl')) assert x == simple x = pickle.load (open ('simple2 .pkl ',' rb ')) assert x == simple

Selon la documentation, vous êtes censé ouvrir des pickles binaires en utilisant le mode 'rb', mais comme vous pouvez le constater, cela fonctionne dans les deux sens..

Voyons comment pickle gère le graphe d'objet complexe.

pickle.dumps (complex) "(dp1 \ nS'a '\ nccopy_reg \ n_reconstructor \ np2 \ n (c__main __ \ nA \ np3 \ nc__bâtiment __ \ np4 \ nNtRp5 \ n (dp6 \ nS \ nS7 \ np7 \ n) dp8 \ nS'text '\ np9 \ nS'string' \ np10 \ nsS'none '\ np11 \ nNSS'boolean' \ np12 \ nI12 \ nsSnumber '\ np13 \ nF3.43999999999999999999 \ nsS'int_list' \ np14 \ n (lp15 \ nI1 \ naI2 \ naI3 \ nassbsS'when '\ np16 \ ncdatetime \ ndatetime \ np17 \ n (S' \\ x07 \\ xo0 \\ x07 \\ x00 \\ x00 \\ x00 \\ x00 \\ x00 \\ x00 \\ x00 '\ ntRp18 \ ns. "pickle.dumps (complexe, protocole = pickle.HIGHEST_PROTOCOL)' \ x80 \ x02 q \ x01 (U \ x01ac__main __ \ nA \ nq \ x02) \ x81q \ x03  q \ x04U \ x06simpleq \ x05 q \ x06 (U \ x04textq] q \ x0b (K \ x01K \ x02K \ x03eusbU \ x04whenq \ x0ccdatetime \ ndatetime \ nq \ rU \ n \ x07 \ x0 \ x0 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x00 \ x85 \ x0. .dump (complexe, ouvert ('complex1.pkl', 'w')) pickle.dump (complexe, ouvert ('complex2.pkl', 'wb'), protocole = pickle.HIGHEST_PROTOCOL) ls -la comp *. * -rw-r - r-- 1 gigi de personnel 327 9 mars 02:58 complex1.pkl -rw-r - r-- 1 gigi de personnel 171 9 mars 02:58 complex2.pkl

L'efficacité du protocole binaire est encore plus grande avec les graphes d'objets complexes.

JSON

JSON (JavaScript Object Notation) fait partie de la bibliothèque standard Python depuis Python 2.5. Je vais considérer cela comme un format natif. C'est un format texte qui est le roi non officiel du Web en ce qui concerne la sérialisation des objets. Son système de types modélise naturellement JavaScript, il est donc assez limité. 

Sérialisons et désérialisons les graphiques d'objets simples et complexes et voyons ce qui se passe. L'interface est presque identique à l'interface pickle. Tu as déverser(), décharges (), charge(), et charges() les fonctions. Mais, il n'y a pas de protocole à sélectionner et il y a beaucoup d'arguments optionnels pour contrôler le processus. Commençons simplement en vidant le graphe d'objet simple sans arguments particuliers:

importer json print json.dumps (simple) "text": "chaîne", "none": null, "boolean": true, "nombre": 3.44, "int_list": [1, 2, 3]

La sortie semble assez lisible, mais il n'y a pas d'indentation. Pour un graphe d'objet plus grand, cela peut poser problème. Retirons la sortie:

print json.dumps (simple, indent = 4) "text": "chaîne", "none": null, "boolean": true, "number": 3.44, "int_list": [1, 2, 3]

Cela a l'air beaucoup mieux. Passons au graphe d'objet complexe.

json.dumps (complexe) ------------------------------------------------ ------------------------------- TypeError Traceback (l'appel le plus récent en dernier)  dans () ----> 1 json.dumps (complex) /usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.pyc dans les décharges (obj, skipkeys, Ensure_ascii, check_circular, allow_nan, cls, indent, séparateurs, encodage, défaut, sort_keys, ** kw) 241 cls est Aucun et indent est Aucun et séparateurs est Aucun et 242 encodage == 'utf-8' et La valeur par défaut est None et non pas sort_keys ni kw): -> 243 return _default_encoder.encode (obj) 244 si cls vaut None: 245 cls = JSONEncoder /usr/local/Cellar/python/2.7.10/Frameworks/Python.framework /Versions/2.7/lib/python2.7/json/encoder.pyc dans encoder (self, o) 205 # exceptions ne sont pas aussi détaillées. L'appel à la liste doit être approximativement équivalent à # PySequence_Fast que ".join () ferait. -> 207 chunks = self.iterencode (o, _one_shot = True) 208 sinon isinstance (chunks, (liste, tuple)) : 209 morceaux = liste (morceaux) /usr/local/Cellar/python/2.7.10/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/encoder.pyc dans Iterencode (self, o, _one_shot) ) 268 self.key_separator, self.item_separator, self.sort_keys, 269 self.skipkeys, _one_shot) -> 270 retourne _iterencode (o, 0) 271 272 def _make_iterencode (marqueurs, _default, _encoder, _encent, _indent, _flostr, / usr / local / Cellar / python / 2.7.10 / Frameworks / Python.framework / Versions / 2.7 / lib / python2.7 / json / encoder.pyc par défaut (self, o) 182 183 "" "-> 184 soulève TypeError ( repr (o) + "n'est pas sérialisable JSON") 185 186 def codé (self, o): TypeError: <__main__.A object at 0x10f367cd0> n'est pas sérialisable JSON

Whoa! Cela ne semble pas bon du tout. Qu'est-il arrivé? Le message d'erreur est que l'objet A n'est pas sérialisable JSON. N'oubliez pas que JSON a un système de types très limité et qu'il ne peut pas sérialiser automatiquement les classes définies par l'utilisateur. La solution consiste à sous-classer la classe JSONEncoder utilisée par le module json et à implémenter le défaut() qui est appelé à chaque fois que l'encodeur JSON rencontre un objet qu'il ne peut pas sérialiser. 

Le travail de l'encodeur personnalisé consiste à le convertir en un graphe d'objet Python que l'encodeur JSON peut encoder. Dans ce cas, nous avons deux objets qui nécessitent un encodage spécial: le date / heure objet et la classe A. L'encodeur suivant fait le travail. Chaque objet spécial est converti en un dict où la clé est le nom du type entouré de dunders (double underscores). Ce sera important pour le décodage. 

depuis datetime import datetime import classe json CustomEncoder (json.JSONEncoder): def par défaut (self, o): if isinstance (o, datetime): return '__datetime__': o.replace (microsecond = 0) .isoformat () return '__  __'. format (o .__ classe __.__ nom__): o .__ dict__

Essayons encore avec notre encodeur personnalisé:

serialized = json.dumps (complexe, indent = 4, cls = CustomEncoder) print serialisé "a": "__A__": "simple": "texte": "chaîne", "aucune": null, "booléen ": true," number ": 3.44," int_list ": [1, 2, 3]," when ": " __datetime__ ":" 2016-03-07T00: 00: 00 "

C'est beau. Le graphe d'objet complexe a été sérialisé correctement et les informations de type d'origine des composants ont été conservées via les clés: "__A__" et "__datetime__". Si vous utilisez dunders pour vos noms, vous devez proposer une convention différente pour désigner les types spéciaux..

Décodons le graphe d'objet complexe.

> deserialized = json.loads (serialized)> deserialized == complex False

Hmmm, la désérialisation a fonctionné (pas d'erreur), mais c'est différent du graphe d'objet complexe d'origine que nous avons sérialisé. Quelque chose ne va pas. Jetons un coup d'oeil au graphe d'objet désérialisé. Je vais utiliser le empreinte fonction de la empreinte module pour l'impression jolie.

> à partir de pprint import pprint> pprint (désérialisé) u'a ': u' __ A__ ': u'simple': u'boolean ': True, u'int_list': [1, 2, 3], u 'none': None, u'number ': 3.44, u'text': u'string ', u'when': u '__ datetime__': u'2016-03-07T00: 00: 00 ' 

D'accord. Le problème est que le module json ne sait rien de la classe A ni même de l’objet datetime standard. Il désérialise tout par défaut vers l'objet Python qui correspond à son système de types. Pour revenir à un graphe d'objet Python riche, vous avez besoin d'un décodage personnalisé.. 

Il n’est pas nécessaire de créer une sous-classe de décodeur personnalisée. le charge() et charges() les fonctions fournissent le paramètre "object_hook" qui vous permet de fournir une fonction personnalisée qui convertit les dicts en objets. 

def decode_object (o): si '__A__' dans o: a = A () a .__ dict __. update (o ['__ A__']) renvoie un elif '__datetime__' dans o: renvoie datetime.strptime (o ['__ datetime__' ], '% Y-% m-% dT% H:% M:% S') retour o

Décodons en utilisant le decode_object () fonctionner en tant que paramètre de la charges() paramètre object_hook.

> deserialized = json.loads (serialized, object_hook = decode_object)> print deserialized u'a ': <__main__.A object at 0x10d984790>, u'when ': datetime.datetime (2016, 3, 7, 0, 0)> deserialized == complex True

Conclusion

Dans la première partie de ce didacticiel, vous avez abordé le concept général de sérialisation et de désérialisation des objets Python et exploré les bases de la sérialisation des objets Python à l'aide de Pickle et JSON.. 

Dans la deuxième partie, vous en apprendrez plus sur le YAML, les problèmes de performances et de sécurité, et un rapide aperçu des schémas de sérialisation supplémentaires..

Apprendre le python

Apprenez Python avec notre guide complet de tutoriel sur Python, que vous soyez débutant ou que vous soyez un codeur chevronné cherchant à acquérir de nouvelles compétences..