web-dev-qa-db-fra.com

Rendre un objet JSON sérialisable avec un encodeur standard

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].

53
leonsas

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\"}"

Magick est ici

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).

Portablity to Python 3

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
66
martineau

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>>"
}
11
Aravindan Ve

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 ]
4
Yoav Kleinberger

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)
1
ribamar

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.

0
Sławomir Lenart

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.

0
blakev