La guerre de la sérialisation a plus ou moins été gagnée par JSON. Le XML est relégué aux documents très riches et aux systèmes legacy. Le YML est cantonné a des niches (et en plus souvent des niches en Ruby, c’est dire !). Et les formats binaires, sont gardés pour les besoins de perf. Le reste, c’est du JSON, du JSON et du JSON.
Seulement le JSON ne permet pas de sauvegarder des dates, seulement des chaînes, des entiers, des booléens et null. Heureusement on peut créer son propre dialecte au dessus de JSON pour y remédier, mais il faut avoir un un parseur qui le gère.
En Python on peut créer sa propre classe d’encodeur et décodeur de JSON et donc techniquement ajouter n’importe quel type.
Voici une recette pour en créer un qui gère le type datetime
de manière transparente :
import re import json from datetime import datetime # On hérite simplement de l'encodeur de base pour faire son propre encodeur class JSONEncoder(json.JSONEncoder): # Cette méthode est appelée pour serialiser les objets en JSON def default(self, obj): # Si l'objet est de type datetime, on retourne une chaîne formatée # représentant l'instant de manière classique # ex: "2014-03-09 19:51:32.7689" if isinstance(obj, datetime): return obj.strftime('%Y-%m-%d %H:%M:%S.%f') return json.JSONEncoder.default(self, obj) # On fait l'opération exactement inverse pour le décodeur class JSONDecoder(json.JSONDecoder): # On écrase la méthode qui permet de décoder les paires clé / valeur # du format JSON afin que chaque valeur passe par notre moulinette def object_pairs_hook(self, obj): return dict((k, self.decode_on_match(v)) for k, v in obj) # notre moulinette def decode_on_match(self, obj): # une petite regex permet de savoir si la chaine est une date # sérialisée selon notre format précédent match = re.search(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}', unicode(obj)) # si oui, on parse et on retourne le datetime if match: return datetime.strptime(match.string, self.datetime_format) # sinon on retourne l'objet tel quel return obj # On se fait des raccourcis pour loader et dumper le json def json_dumps(data): return JSONEncoder().encode(data) def json_loads(string): return JSONDecoder().decode(string) |
Usage :
>>> res = json_dumps({'test': datetime(2000, 1, 1, 1, 1, 1), 'autre': [True, 1]}) >>> print(type(res), res) (<type 'str'>, '{"test": "2000-01-01 01:01:01.000000", "autre": [true, 1]}') >>> res = json_loads(res) >>> print(type(res), res) (<type 'dict'>, {u'test': u'2000-01-01 01:01:01.000000', u'autre': [True, 1]}) |
Minibelt contient une version un peu plus élaborée de ce code qui prend en compte les types date
, time
et timedelta
ainsi que pas mal d’options de configuration.
isinstance
? Erf…Pourquoi ne pas utiliser ça dans le sérialiseur plutôt ?
Rapide, pas cher, standard … et ça se fait bien manger par toutes les libs de tous les langages. Et puis après, il suffit d’avoir le désérialiseur idoine.
Comme Badu, je préfère la méthode .isoformat(), il vaut mieux garder les standards… ISO 8601 (UTC format)
+1, mieux vaut s’en tenir à isoformat, surtout qu’avec la méthode actuelle on perdrait les timezone des datetime conscient, ce qui est grave.
Tous des points parfaitement valides. Effectivement, ce seraient de bonnes améliorations.
Dans l’exemple, la désérialisation de la date ne renvoie pas un
datetime
. C’est toujours une chaîne de caractère.J’ai pas trop compris pourquoi (peut-être en as-tu parlé dans le guide ultime et définitif sur la POO) mais apparemment la méthode
decode
n’utilise pas l’attributobject_pairs_hook
de l’instance mais de la classe parente. Pour corriger le tir, j’ai fait ceci:Avec ça, le
print(donnees_loads)
renvoie bien undatetime
.Mais j’avoue ne pas comprendre pourquoi cet attribut n’est pas pris en compte, alors que si je redéfini l’attribut
indent
dans l’encodeur, lui est bien pris en compte.Juste 2 petites remarques: Dans la méthode
decode_on_match
de tonJSONDecoder
, tu n’as pas définiself.datetime_format
. Etobject_pairs_hook
n’est pas à la base une méthode mais un attribut (qui attend un callable).PS: Désolé pour le formatage de mon code. J’ai bien utilisé les balises
pre
mais dans le pré-visualiseur le code n’apparaît pas correctement.Une autre solution que je trouve simple et lisible :
<
pre>from datetime import datetime
def json_encoder(o):
if isinstance(o, datetime):
return {“isoformat”: o.isoformat(), “class“: str(o.class)}
if isinstance(o, (CustomClass1, CustomClass2)):
dic = o.dict
dic[“class“] = str(o.class)
return dic
raise TypeError(“CustomClass or datetime expected.”)