La méthode habituelle de sérialisation d'objets JSON personnalisés non sérialisables consiste à sous-classer json.JSONEncoder
, puis à transmettre un encodeur personnalisé aux vidages.
Cela ressemble habituellement à ceci:
class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, foo):
return obj.to_json()
return json.JSONEncoder.default(self, obj)
print json.dumps(obj, cls = CustomEncoder)
Ce que j'essaie de faire, c'est de rendre quelque chose de sérialisable avec l'encodeur par défaut. J'ai regardé autour de moi mais je n'ai rien trouvé. Je pensais qu'il y aurait un champ dans lequel le codeur se pencherait pour déterminer le codage JSON. Quelque chose de semblable à __str__
. Peut-être un champ __json__
. Y at-il quelque chose comme ceci en python?
Je souhaite que l'une des classes d'un module que je crée soit sérialisable JSON pour tous ceux qui l'utilisent sans s'inquiéter de la mise en oeuvre de leurs propres encodeurs personnalisés [triviaux].
Comme je l'ai dit dans un commentaire à votre question, après avoir examiné le code source du module json
, il ne semble pas se prêter à faire ce que vous voulez. Cependant, l’objectif peut être atteint avec ce que l’on appelle monkey-patching (Voir question Qu'est-ce qu'un patch de singe? ) . Cela pourrait être Ceci est effectué dans le script d'initialisation __init__.py
de votre paquet et aurait une incidence sur toutes les sérialisations ultérieures du module json
puisque les modules ne sont généralement chargés qu'une seule fois et que le résultat est mis en cache dans sys.modules
.
Le correctif change la méthode default
du codeur Json par défaut, la default()
par défaut.
Voici un exemple implémenté en tant que module autonome pour des raisons de simplicité:
Module: make_json_serializable.py
""" Module that monkey-patches json module when it's imported so
JSONEncoder.default() automatically checks for a special "to_json()"
method and uses it to encode the object if found.
"""
from json import JSONEncoder
def _default(self, obj):
return getattr(obj.__class__, "to_json", _default.default)(obj)
_default.default = JSONEncoder.default # Save unmodified default.
JSONEncoder.default = _default # Replace it.
Son utilisation est triviale puisque le patch est appliqué simplement en important le module.
Exemple de script client:
import json
import make_json_serializable # apply monkey-patch
class Foo(object):
def __init__(self, name):
self.name = name
def to_json(self): # New special method.
""" Convert to JSON format string representation. """
return '{"name": "%s"}' % self.name
foo = Foo('sazpaz')
print(json.dumps(foo)) # -> "{\"name\": \"sazpaz\"}"
Pour conserver les informations de type d'objet, la méthode spéciale peut également les inclure dans la chaîne renvoyée:
return ('{"type": "%s", "name": "%s"}' %
(self.__class__.__name__, self.name))
Qui produit le JSON suivant qui inclut maintenant le nom de la classe:
"{\"type\": \"Foo\", \"name\": \"sazpaz\"}"
Mieux encore que de remplacer la fonction default()
de remplacement par une méthode nommée spécialement, ce serait en mesure de sérialiser la plupart des objets Python automatiquement, y compris les instances de classe définies par l'utilisateur, sans qu'il soit nécessaire d'ajouter une méthode spéciale. Après avoir étudié plusieurs solutions, les options suivantes, qui utilisent le module pickle
, me paraissaient le plus proches de cet idéal:
Module: make_json_serializable2.py
""" Module that imports the json module and monkey-patches it so
JSONEncoder.default() automatically pickles any Python objects
encountered that aren't standard JSON data types.
"""
from json import JSONEncoder
import pickle
def _default(self, obj):
return {'_python_object': pickle.dumps(obj)}
JSONEncoder.default = _default # Replace with the above.
Bien sûr, tout ne peut pas être décapé, types d'extension par exemple. Cependant, il existe des moyens définis pour les traiter via le protocole de pickle en écrivant des méthodes spéciales - semblables à celles que vous avez suggérées et que j'ai décrites précédemment - mais cela serait probablement nécessaire dans un nombre de cas bien moindre.
Quoi qu'il en soit, l'utilisation du protocole pickle signifie également qu'il serait assez facile de reconstruire l'objet Python d'origine en fournissant un argument de fonction object_hook
personnalisé à tout appel json.loads()
qui recherchait une clé '_python_object'
dans le dictionnaire transmis.
def as_python_object(dct):
if '_python_object' in dct:
return pickle.loads(str(dct['_python_object']))
return dct
pyobj = json.loads(json_str, object_hook=as_python_object)
Si cela doit être fait à plusieurs endroits, il peut être intéressant de définir une fonction wrapper fournissant automatiquement l'argument de mot clé supplémentaire:
json_pkloads = functools.partial(json.loads, object_hook=as_python_object)
pyobj = json_pkloads(json_str)
Naturellement, cela pourrait également être corrigé dans le module json
, ce qui en ferait la fonction par défaut object_hook
(au lieu de None
).
J'ai eu l'idée d'utiliser pickle
d'une réponse de answer de Raymond Hettinger à une autre question de sérialisation JSON, que je considère exceptionnellement crédible ainsi qu'une source officielle (comme dans le développeur Python).
Le code ci-dessus ne fonctionne pas comme le montre Python 3, car json.dumps()
renvoie un objet bytes
que JSONEncoder
ne peut pas gérer. Cependant, l'approche est toujours valable. Une solution simple au problème consiste à latin1
"décoder" la valeur renvoyée par pickle.dumps()
, puis à "l'encoder" à partir de latin1
avant de la transmettre à pickle.loads()
dans la fonction as_python_object()
. Cela fonctionne parce que les chaînes binaires arbitraires sont valides latin1
qui peut toujours être décodé en Unicode, puis à nouveau encodé dans la chaîne d'origine (comme indiqué dans cette réponse par Sven Marnach ).
(Bien que ce qui suit fonctionne bien dans Python 2, le décodage et le codage latin1
qu'il fait est superflu.)
from decimal import Decimal
class PythonObjectEncoder(json.JSONEncoder):
def default(self, obj):
return {'_python_object': pickle.dumps(obj).decode('latin1')}
def as_python_object(dct):
if '_python_object' in dct:
return pickle.loads(dct['_python_object'].encode('latin1'))
return dct
data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'},
Decimal('3.14')]
j = json.dumps(data, cls=PythonObjectEncoder, indent=4)
data2 = json.loads(j, object_hook=as_python_object)
assert data == data2 # both should be same
Vous pouvez étendre la classe dict comme suit:
#!/usr/local/bin/python3
import json
class Serializable(dict):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# hack to fix _json.so make_encoder serialize properly
self.__setitem__('dummy', 1)
def _myattrs(self):
return [
(x, self._repr(getattr(self, x)))
for x in self.__dir__()
if x not in Serializable().__dir__()
]
def _repr(self, value):
if isinstance(value, (str, int, float, list, Tuple, dict)):
return value
else:
return repr(value)
def __repr__(self):
return '<%s.%s object at %s>' % (
self.__class__.__module__,
self.__class__.__name__,
hex(id(self))
)
def keys(self):
return iter([x[0] for x in self._myattrs()])
def values(self):
return iter([x[1] for x in self._myattrs()])
def items(self):
return iter(self._myattrs())
Maintenant, pour rendre vos classes sérialisables avec l'encodeur normal, étendez 'Serializable':
class MySerializableClass(Serializable):
attr_1 = 'first attribute'
attr_2 = 23
def my_function(self):
print('do something here')
obj = MySerializableClass()
print(obj)
imprimera quelque chose comme:
<__main__.MySerializableClass object at 0x1073525e8>
print(json.dumps(obj, indent=4))
imprimera quelque chose comme:
{
"attr_1": "first attribute",
"attr_2": 23,
"my_function": "<bound method MySerializableClass.my_function of <__main__.MySerializableClass object at 0x1073525e8>>"
}
Je suggère de mettre le hack dans la définition de la classe. Ainsi, une fois la classe définie, elle prend en charge JSON. Exemple:
import json
class MyClass( object ):
def _jsonSupport( *args ):
def default( self, xObject ):
return { 'type': 'MyClass', 'name': xObject.name() }
def objectHook( obj ):
if 'type' not in obj:
return obj
if obj[ 'type' ] != 'MyClass':
return obj
return MyClass( obj[ 'name' ] )
json.JSONEncoder.default = default
json._default_decoder = json.JSONDecoder( object_hook = objectHook )
_jsonSupport()
def __init__( self, name ):
self._name = name
def name( self ):
return self._name
def __repr__( self ):
return '<MyClass(name=%s)>' % self._name
myObject = MyClass( 'Magneto' )
jsonString = json.dumps( [ myObject, 'some', { 'other': 'objects' } ] )
print "json representation:", jsonString
decoded = json.loads( jsonString )
print "after decoding, our object is the first in the list", decoded[ 0 ]
Le problème avec la substitution de JSONEncoder().default
est que vous ne pouvez le faire qu’une seule fois. Si vous tombez sur quelque chose, un type de données spécial qui ne fonctionne pas avec ce modèle (comme si vous utilisiez un codage étrange). Avec le modèle ci-dessous, vous pouvez toujours rendre votre classe JSON sérialisable, à condition que le champ de classe que vous souhaitez sérialiser soit lui-même sérialisable (et qu'il puisse être ajouté à une liste python, presque rien). Sinon, vous devez appliquer de manière récursive le même motif à votre champ json (ou en extraire les données sérialisables):
# base class that will make all derivatives JSON serializable:
class JSONSerializable(list): # need to derive from a serializable class.
def __init__(self, value = None):
self = [ value ]
def setJSONSerializableValue(self, value):
self = [ value ]
def getJSONSerializableValue(self):
return self[1] if len(self) else None
# derive your classes from JSONSerializable:
class MyJSONSerializableObject(JSONSerializable):
def __init__(self): # or any other function
# ....
# suppose your__json__field is the class member to be serialized.
# it has to be serializable itself.
# Every time you want to set it, call this function:
self.setJSONSerializableValue(your__json__field)
# ...
# ... and when you need access to it, get this way:
do_something_with_your__json__field(self.getJSONSerializableValue())
# now you have a JSON default-serializable class:
a = MyJSONSerializableObject()
print json.dumps(a)
Pour un environnement de production, préparez plutôt le propre module de json
avec votre propre encodeur personnalisé, afin de bien indiquer que vous remplacez quelque chose . Monkey-patch n'est pas recommandé, mais vous pouvez le faire en mode test.
Par exemple,
class JSONDatetimeAndPhonesEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (datetime.date, datetime.datetime)):
return obj.date().isoformat()
Elif isinstance(obj, basestring):
try:
number = phonenumbers.parse(obj)
except phonenumbers.NumberParseException:
return json.JSONEncoder.default(self, obj)
else:
return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.NATIONAL)
else:
return json.JSONEncoder.default(self, obj)
tu veux:
payload = json.dumps (your_data, cls = JSONDatetimeAndPhonesEncoder)
ou:
payload = your_dumps (your_data)
ou:
payload = your_json.dumps (your_data)
cependant, dans l'environnement de test, allez-y:
@pytest.fixture(scope='session', autouse=True)
def testenv_monkey_patching():
json._default_encoder = JSONDatetimeAndPhonesEncoder()
qui appliquera votre encodeur à toutes les occurrences json.dumps
.
Je ne comprends pas pourquoi vous ne pouvez pas écrire une fonction serialize
pour votre propre classe? Vous implémentez l'encodeur personnalisé dans la classe elle-même et autorisez "personnes" à appeler la fonction de sérialisation qui renverra essentiellement self.__dict__
avec les fonctions supprimées.
modifier:
Cette question convient avec moi que le moyen le plus simple est d'écrire votre propre méthode et de renvoyer les données json sérialisées que vous voulez. Ils recommandent également d'essayer jsonpickle, mais vous ajoutez maintenant une dépendance supplémentaire pour la beauté lorsque la solution appropriée est intégrée.