Comment rendre une classe Python sérialisable?
Un cours simple:
class FileItem:
def __init__(self, fname):
self.fname = fname
Que dois-je faire pour pouvoir obtenir les résultats suivants:
json.dumps()
Sans erreur (FileItem instance at ... is not JSON serializable
)
Avez-vous une idée de la sortie attendue? Par exemple ça fera l'affraire?
>>> f = FileItem("/foo/bar")
>>> magic(f)
'{"fname": "/foo/bar"}'
Dans ce cas, vous pouvez simplement appeler json.dumps(f.__dict__)
.
Si vous souhaitez une sortie plus personnalisée, vous devrez sous-classe JSONEncoder
et implémenter votre propre sérialisation personnalisée.
Pour un exemple trivial, voir ci-dessous.
>>> from json import JSONEncoder
>>> class MyEncoder(JSONEncoder):
def default(self, o):
return o.__dict__
>>> MyEncoder().encode(f)
'{"fname": "/foo/bar"}'
Ensuite, vous transmettez cette classe à la méthode json.dumps()
sous la forme cls
kwarg:
json.dumps(cls=MyEncoder)
Si vous souhaitez également décoder, vous devrez fournir un object_hook
personnalisé à la classe JSONDecoder
. Par exemple.
>>> def from_json(json_object):
if 'fname' in json_object:
return FileItem(json_object['fname'])
>>> f = JSONDecoder(object_hook = from_json).decode('{"fname": "/foo/bar"}')
>>> f
<__main__.FileItem object at 0x9337fac>
>>>
Voici une solution simple pour une fonctionnalité simple:
.toJSON()
MéthodeAu lieu d'une classe sérialisable JSON, implémentez une méthode de sérialiseur:
import json
class Object:
def toJSON(self):
return json.dumps(self, default=lambda o: o.__dict__,
sort_keys=True, indent=4)
Donc, vous appelez juste pour sérialiser:
me = Object()
me.name = "Onur"
me.age = 35
me.dog = Object()
me.dog.name = "Apollo"
print(me.toJSON())
affichera:
{
"age": 35,
"dog": {
"name": "Apollo"
},
"name": "Onur"
}
Pour les classes plus complexes, vous pouvez utiliser l'outil jsonpickle :
jsonpickle est une bibliothèque Python pour la sérialisation et la désérialisation d'objets Python complexes vers et depuis JSON.
Les bibliothèques Python standard pour coder Python en JSON, telles que json, simplejson et demjson de stdlib, ne peuvent gérer que les primitives Python ayant un équivalent JSON direct (p. Ex. Dicts, listes, chaînes, entiers, etc.). jsonpickle repose sur ces bibliothèques et permet la sérialisation de structures de données plus complexes en JSON. jsonpickle est hautement configurable et extensible, permettant à l'utilisateur de choisir le backend JSON et d'ajouter des backends supplémentaires.
La plupart des réponses impliquent de changer l'appel en json.dumps () , ce qui n'est pas toujours possible ou souhaitable (cela peut arriver dans un composant cadre par exemple).
Si vous voulez pouvoir appeler json.dumps (obj) tel quel, une solution simple consiste à hériter de dict :
class FileItem(dict):
def __init__(self, fname):
dict.__init__(self, fname=fname)
f = FileItem('tasks.txt')
json.dumps(f) #No need to change anything here
Cela fonctionne si votre classe est juste une représentation de base de données. Pour les choses plus délicates, vous pouvez toujours définir les clés de manière explicite.
Une autre option consiste à intégrer le dumping JSON dans sa propre classe:
import json
class FileItem:
def __init__(self, fname):
self.fname = fname
def __repr__(self):
return json.dumps(self.__dict__)
Ou encore mieux, sous-classez la classe FileItem d'une classe JsonSerializable
:
import json
class JsonSerializable(object):
def toJson(self):
return json.dumps(self.__dict__)
def __repr__(self):
return self.toJson()
class FileItem(JsonSerializable):
def __init__(self, fname):
self.fname = fname
Essai:
>>> f = FileItem('/foo/bar')
>>> f.toJson()
'{"fname": "/foo/bar"}'
>>> f
'{"fname": "/foo/bar"}'
>>> str(f) # string coercion
'{"fname": "/foo/bar"}'
J'aime bien la réponse de Onur , mais je voudrais développer une méthode optionnelle toJSON()
pour que les objets se sérialisent:
def dumper(obj):
try:
return obj.toJSON()
except:
return obj.__dict__
print json.dumps(some_big_object, default=dumper, indent=2)
L’autre jour, j’ai rencontré ce problème et j’ai implémenté une version plus générale d’un objet Encoder for Python qui peut gérer les objets imbriqués et les champs hérités :
import json
import inspect
class ObjectEncoder(json.JSONEncoder):
def default(self, obj):
if hasattr(obj, "to_json"):
return self.default(obj.to_json())
Elif hasattr(obj, "__dict__"):
d = dict(
(key, value)
for key, value in inspect.getmembers(obj)
if not key.startswith("__")
and not inspect.isabstract(value)
and not inspect.isbuiltin(value)
and not inspect.isfunction(value)
and not inspect.isgenerator(value)
and not inspect.isgeneratorfunction(value)
and not inspect.ismethod(value)
and not inspect.ismethoddescriptor(value)
and not inspect.isroutine(value)
)
return self.default(d)
return obj
Exemple:
class C(object):
c = "NO"
def to_json(self):
return {"c": "YES"}
class B(object):
b = "B"
i = "I"
def __init__(self, y):
self.y = y
def f(self):
print "f"
class A(B):
a = "A"
def __init__(self):
self.b = [{"ab": B("y")}]
self.c = C()
print json.dumps(A(), cls=ObjectEncoder, indent=2, sort_keys=True)
Résultat:
{
"a": "A",
"b": [
{
"ab": {
"b": "B",
"i": "I",
"y": "y"
}
}
],
"c": {
"c": "YES"
},
"i": "I"
}
Ajoutez simplement la méthode to_json
à votre classe comme ceci:
def to_json(self):
return self.message # or how you want it to be serialized
Et ajoutez ce code (de cette réponse ), quelque part au sommet de tout:
from json import JSONEncoder
def _default(self, obj):
return getattr(obj.__class__, "to_json", _default.default)(obj)
_default.default = JSONEncoder().default
JSONEncoder.default = _default
Cela va corriger le module json lorsqu’il est importé, donc JSONEncoder.default () recherche automatiquement une méthode spéciale "to_json ()" Et l’utilise pour coder l’objet, le cas échéant.
Tout comme Onur l'a dit, mais cette fois-ci, vous n'avez pas à mettre à jour tous les json.dumps()
de votre projet.
import simplejson
class User(object):
def __init__(self, name, mail):
self.name = name
self.mail = mail
def _asdict(self):
return self.__dict__
print(simplejson.dumps(User('alice', '[email protected]')))
si vous utilisez la norme json
, vous devez définir une fonction default
import json
def default(o):
return o._asdict()
print(json.dumps(User('alice', '[email protected]'), default=default))
Cette classe peut faire l'affaire, elle convertit un objet en json standard.
import json
class Serializer(object):
@staticmethod
def serialize(object):
return json.dumps(object, default=lambda o: o.__dict__.values()[0])
usage:
Serializer.serialize(my_object)
travaillant dans python2.7
et python3
.
json
est limité en termes d’objets qu’il peut imprimer, et jsonpickle
(vous aurez peut-être besoin d’un pip install jsonpickle
) est limité en termes d’indéscriptibilité du texte. Si vous souhaitez inspecter le contenu d'un objet dont vous ne pouvez pas changer la classe, je ne pouvais toujours pas trouver un moyen plus simple que:
import json
import jsonpickle
...
print json.dumps(json.loads(jsonpickle.encode(object)), indent=2)
Notez qu'ils ne peuvent toujours pas imprimer les méthodes d'objet.
jaraco a donné une réponse plutôt soignée. J'ai eu besoin de réparer quelques petites choses, mais cela fonctionne:
# Your custom class
class MyCustom(object):
def __json__(self):
return {
'a': self.a,
'b': self.b,
'__python__': 'mymodule.submodule:MyCustom.from_json',
}
to_json = __json__ # supported by simplejson
@classmethod
def from_json(cls, json):
obj = cls()
obj.a = json['a']
obj.b = json['b']
return obj
# Dumping and loading
import simplejson
obj = MyCustom()
obj.a = 3
obj.b = 4
json = simplejson.dumps(obj, for_json=True)
# Two-step loading
obj2_dict = simplejson.loads(json)
obj2 = MyCustom.from_json(obj2_dict)
# Make sure we have the correct thing
assert isinstance(obj2, MyCustom)
assert obj2.__dict__ == obj.__dict__
Notez que nous avons besoin de deux étapes pour le chargement. Pour l'instant, la propriété __python__
n'est pas utilisée.
En utilisant la méthode de AlJohri , je vérifie la popularité des approches:
Sérialisation (Python -> JSON):
to_json
: 266,595 le 2018-06-27toJSON
: 96,307 sur 2018-06-27__json__
: 8 504 le 2018-06-27for_json
: 6,937 sur 2018-06-27Désérialisation (JSON -> Python):
from_json
: 226.101 sur 2018-06-27import json
class Foo(object):
def __init__(self):
self.bar = 'baz'
self._qux = 'flub'
def somemethod(self):
pass
def default(instance):
return {k: v
for k, v in vars(instance).items()
if not str(k).startswith('_')}
json_foo = json.dumps(Foo(), default=default)
assert '{"bar": "baz"}' == json_foo
print(json_foo)
jsonweb semble être la meilleure solution pour moi. Voir http://www.jsonweb.info/en/latest/
from jsonweb.encode import to_object, dumper
@to_object()
class DataModel(object):
def __init__(self, id, value):
self.id = id
self.value = value
>>> data = DataModel(5, "foo")
>>> dumper(data)
'{"__type__": "DataModel", "id": 5, "value": "foo"}'
Si vous utilisez Python3.5 +, vous pouvez utiliser jsons
. Il convertira votre objet (et tous ses attributs de manière récursive) en dict.
import jsons
a_dict = jsons.dump(your_object)
Ou si vous vouliez une ficelle:
a_str = jsons.dumps(your_object)
Ou si votre classe implémenté jsons.JsonSerializable
:
a_dict = your_object.json
Si cela ne vous dérange pas d'installer un paquet, vous pouvez utiliser json-tricks :
pip install json-tricks
Après cela, il vous suffit d’importer dump(s)
à partir de json_tricks
au lieu de json, et cela fonctionnera généralement:
from json_tricks import dumps
json_str = dumps(cls_instance, indent=4)
qui va donner
{
"__instance_type__": [
"module_name.test_class",
"MyTestCls"
],
"attributes": {
"attr": "val",
"dct_attr": {
"hello": 42
}
}
}
Et c'est fondamentalement ça!
Cela fonctionnera très bien en général. Il existe quelques exceptions, par exemple si des choses spéciales se produisent dans __new__
, ou plus de métaclasse se poursuit.
Évidemment, le chargement fonctionne aussi (sinon, quel est le but):
from json_tricks import loads
json_str = loads(json_str)
Cela suppose que module_name.test_class.MyTestCls
peut être importé et n'a pas été modifié de manière non compatible. Vous récupérerez une instance, pas un dictionnaire ou quelque chose du genre, et ce devrait être une copie identique à celle que vous avez sauvegardée.
Si vous voulez personnaliser la manière dont quelque chose est (ou) sérialisée, vous pouvez ajouter des méthodes spéciales à votre classe, comme ceci:
class CustomEncodeCls:
def __init__(self):
self.relevant = 42
self.irrelevant = 37
def __json_encode__(self):
# should return primitive, serializable types like dict, list, int, string, float...
return {'relevant': self.relevant}
def __json_decode__(self, **attrs):
# should initialize all properties; note that __init__ is not called implicitly
self.relevant = attrs['relevant']
self.irrelevant = 12
qui ne sérialise qu’une partie des paramètres d’attributs, à titre d’exemple.
Et en bonus gratuit, vous obtenez une (dé) sérialisation des tableaux numpy, des dates et heures, des cartes ordonnées, ainsi que la possibilité d'inclure des commentaires dans json.
Disclaimer: J'ai créé json_tricks , parce que j'ai eu le même problème que vous.
J'ai rencontré ce problème lorsque j'ai essayé de stocker le modèle de Peewee dans PostgreSQL JSONField
.
Après avoir lutté pendant un moment, voici la solution générale.
La solution de ma solution passe par le code source de Python et réalise que la documentation de code (décrite ici ) explique déjà comment étendre le json.dumps
existant pour prendre en charge d’autres types de données.
Supposons que votre modèle actuel contienne des champs non sérialisables en JSON et que le modèle contenant le champ JSON ressemble à ceci:
class SomeClass(Model):
json_field = JSONField()
Définissez simplement une JSONEncoder
personnalisée comme ceci:
class CustomJsonEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, SomeTypeUnsupportedByJsonDumps):
return < whatever value you want >
return json.JSONEncoder.default(self, obj)
@staticmethod
def json_dumper(obj):
return json.dumps(obj, cls=CustomJsonEncoder)
Et ensuite, utilisez-le dans votre JSONField
comme ci-dessous:
class SomeClass(Model):
json_field = JSONField(dumps=CustomJsonEncoder.json_dumper)
La clé est la méthode default(self, obj)
ci-dessus. Pour chaque réclamation ... is not JSON serializable
que vous recevez de Python, ajoutez simplement du code permettant de gérer le type non sérialisable en JSON (tel que Enum
ou datetime
).
Par exemple, voici comment je supporte une classe héritant de Enum
:
class TransactionType(Enum):
CURRENT = 1
STACKED = 2
def default(self, obj):
if isinstance(obj, TransactionType):
return obj.value
return json.JSONEncoder.default(self, obj)
Enfin, avec le code implémenté comme ci-dessus, vous pouvez simplement convertir n’importe quel modèle Peewee en un objet JSON-compatible comme ci-dessous:
peewee_model = WhateverPeeweeModel()
new_model = SomeClass()
new_model.json_field = model_to_dict(peewee_model)
Bien que le code ci-dessus soit (quelque peu) spécifique à Peewee, mais je pense:
json.dumps
, cette solution fonctionne également avec Python (sans ORM) en général.Toutes les questions, s'il vous plaît poster dans la section commentaires. Merci!
Voici mes 3 cents ...
Ceci illustre la sérialisation json explicite pour un objet python semblable à une arborescence.
Remarque: Si vous voulez réellement un code comme celui-ci, vous pouvez utiliser le twisted FilePath class.
import json, sys, os
class File:
def __init__(self, path):
self.path = path
def isdir(self):
return os.path.isdir(self.path)
def isfile(self):
return os.path.isfile(self.path)
def children(self):
return [File(os.path.join(self.path, f))
for f in os.listdir(self.path)]
def getsize(self):
return os.path.getsize(self.path)
def getModificationTime(self):
return os.path.getmtime(self.path)
def _default(o):
d = {}
d['path'] = o.path
d['isFile'] = o.isfile()
d['isDir'] = o.isdir()
d['mtime'] = int(o.getModificationTime())
d['size'] = o.getsize() if o.isfile() else 0
if o.isdir(): d['children'] = o.children()
return d
folder = os.path.abspath('.')
json.dump(File(folder), sys.stdout, default=_default)
Si vous êtes capable d'installer un paquet, je vous recommande d'essayer dill , qui a parfaitement fonctionné pour mon projet. Une bonne chose à propos de ce paquet est qu'il a la même interface que pickle
. Ainsi, si vous avez déjà utilisé pickle
dans votre projet, vous pouvez simplement le remplacer par dill
et voir si le script s'exécute sans changer de code. C'est donc une solution très économique à essayer!
(Anti-divulgation complète: je ne suis aucunement affiliée à et n’ai jamais contribué au projet Dill.)
Installez le paquet:
pip install dill
Puis éditez votre code pour importer dill
au lieu de pickle
:
# import pickle
import dill as pickle
Exécutez votre script et voyez si cela fonctionne. (Si c'est le cas, vous voudrez peut-être nettoyer votre code afin de ne plus suivre le nom du module pickle
!)
Quelques détails sur les types de données que dill
peut et ne peut pas sérialiser, à partir de la page du projet :
dill
peut combiner les types standard suivants:none, type, bool, int, long, float, complex, str, unicode, Tuple, list, dict, file, buffer, builtin, anciennes et nouvelles classes de style, instances d'anciennes et de nouvelles classes de style, set, frozenset, array, fonctions, exceptions
dill
peut également mariner des types standard plus «exotiques»:fonctions avec rendements, fonctions imbriquées, lambdas, cellule, méthode, unboundmethod, module, code, methodwrapper, dictproxy, descripteur de méthode, getsetdescriptor, memberdescriptor, wrapperdescriptor, xrange, slice, notimplemented, Ellipsis, quitter
dill
ne peut pas encore décaper ces types standard:cadre, générateur, traceback
Je suis venu avec ma propre solution. Utilisez cette méthode, transmettez tout document ( dict , list , ObjectId etc) à sérialiser.
def getSerializable(doc):
# check if it's a list
if isinstance(doc, list):
for i, val in enumerate(doc):
doc[i] = getSerializable(doc[i])
return doc
# check if it's a dict
if isinstance(doc, dict):
for key in doc.keys():
doc[key] = getSerializable(doc[key])
return doc
# Process ObjectId
if isinstance(doc, ObjectId):
doc = str(doc)
return doc
# Use any other custom serializting stuff here...
# For the rest of stuff
return doc
C'est une petite bibliothèque qui sérialise un objet avec tous ses enfants en JSON et le réanalyse:
Je ne vois aucune mention ici de la gestion de version en série ou de backcompat, je vais donc publier ma solution que je utilise depuis un moment. J'ai probablement beaucoup plus de choses à apprendre, en particulier Java et Javascript sont probablement plus matures que moi ici, mais voilà
https://Gist.github.com/andy-d/b7878d0044a4242c0498ed6d67fd50fe
Cela a bien fonctionné pour moi:
class JsonSerializable(object):
def serialize(self):
return json.dumps(self.__dict__)
def __repr__(self):
return self.serialize()
@staticmethod
def dumper(obj):
if "serialize" in dir(obj):
return obj.serialize()
return obj.__dict__
et alors
class FileItem(JsonSerializable):
...
et
log.debug(json.dumps(<my object>, default=JsonSerializable.dumper, indent=2))
J'ai le plus aimé la méthode de Lost Koder. J'ai rencontré des problèmes lorsque j'ai essayé de sérialiser des objets plus complexes dont les membres/méthodes ne sont pas sérialisables. Voici mon implémentation qui fonctionne sur plusieurs objets:
class Serializer(object):
@staticmethod
def serialize(obj):
def check(o):
for k, v in o.__dict__.items():
try:
_ = json.dumps(v)
o.__dict__[k] = v
except TypeError:
o.__dict__[k] = str(v)
return o
return json.dumps(check(obj).__dict__, indent=2)
J'ai choisi d'utiliser des décorateurs pour résoudre le problème de sérialisation d'objet datetime ..__ Voici mon code:
#myjson.py
#Author: jmooremcc 7/16/2017
import json
from datetime import datetime, date, time, timedelta
"""
This module uses decorators to serialize date objects using json
The filename is myjson.py
In another module you simply add the following import statement:
from myjson import json
json.dumps and json.dump will then correctly serialize datetime and date
objects
"""
def json_serial(obj):
"""JSON serializer for objects not serializable by default json code"""
if isinstance(obj, (datetime, date)):
serial = str(obj)
return serial
raise TypeError ("Type %s not serializable" % type(obj))
def FixDumps(fn):
def hook(obj):
return fn(obj, default=json_serial)
return hook
def FixDump(fn):
def hook(obj, fp):
return fn(obj,fp, default=json_serial)
return hook
json.dumps=FixDumps(json.dumps)
json.dump=FixDump(json.dump)
if __name__=="__main__":
today=datetime.now()
data={'atime':today, 'greet':'Hello'}
str=json.dumps(data)
print str
En important le module ci-dessus, mes autres modules utilisent json de manière normale (sans spécifier le mot clé par défaut) pour sérialiser des données contenant des objets date-heure. Le code du sérialiseur datetime est appelé automatiquement pour json.dumps et json.dump.