Rendre l'objet JSON sérialisable avec un encodeur régulier

la méthode habituelle de JSON-sérializing custom Non-serializable objects consiste à utiliser la sous-classe json.JSONEncoder puis à passer un encodeur custom à dumps.

il 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 sérialisable avec l'encodeur par défaut. J'ai cherché partout, mais je n'ai rien trouvé. Ma pensée est qu'il y aurait un domaine dans lequel le codeur regarde pour déterminer l'encodage json. Quelque similaire à __str__ . Peut-être un champ __json__ . Il y a quelque chose comme ça en python?

je veux faire une classe d'un module que je fais pour être sérialisable JSON à tous ceux qui utilisent le paquet sans se soucier de mettre en œuvre leurs propres encodeurs personnalisés [trivial].

51
demandé sur leonsas 2013-08-28 06:04:00

5 réponses

comme je l'ai dit dans un commentaire à votre question, après avoir regardé le code source du module json , il ne semble pas se prêter à faire ce que vous voulez. Toutefois, l'objectif pourrait être atteint par ce qui est connu sous le nom de "patching (voir la question Qu'est-ce qu'un patch de singe? ). Cela pourrait être fait dans le script d'initialisation __init__.py de votre paquet et affecterait tous les json module de sérialisation étant donné que les modules sont généralement seulement chargé une fois et le résultat est mis en cache dans sys.modules .

le correctif modifie la méthode par défaut de l'encodeur json default -la méthode par défaut default() .

voici un exemple implémenté comme un module autonome pour simplifier:

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é par simplement importer 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 l'information de type d'objet, la méthode spéciale peut également l'inclure dans la chaîne retournée:

        return ('{"type": "%s", "name": "%s"}' %
                 (self.__class__.__name__, self.name))

qui produit le JSON suivant qui comprend maintenant le nom de classe:

"{\"type\": \"Foo\", \"name\": \"sazpaz\"}"

encore mieux que d'avoir le remplacement default() chercher une méthode spécialement nommée, serait pour lui d'être en mesure pour sérialiser la plupart des objets Python automatiquement , y compris les instances de classe définies par l'utilisateur, sans avoir besoin d'ajouter une méthode spéciale. Après avoir recherché un certain nombre d'alternatives, ce qui suit qui utilise le module pickle , a semblé le plus proche de cet idéal pour moi:

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é-les types d'extension par exemple. Cependant il y a des moyens définis pour les gérer par le biais du protocole pickle en écrivant des méthodes spéciales-semblables à ce que vous avez suggéré et que j'ai décrit plus tôt-mais faire cela serait probablement nécessaire pour un nombre beaucoup moins élevé de cas.

quoi qu'il en soit, l'utilisation du protocole pickle signifie également qu'il serait assez facile de reconstruire l'objet Python original en fournissant un argument de fonction personnalisé object_hook sur tous les appels json.loads() qui cherchaient une touche '_python_object' dans le dictionnaire passé. Quelque chose comme:

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 dans de nombreux endroits, il pourrait être utile de définir une fonction d'enrubannage qui a fourni 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 être patché en singe dans le module json ainsi, ce qui rend la fonction par défaut object_hook (au lieu de None ).

j'ai eu l'idée d'utiliser pickle à partir d'une réponse par Raymond Hettinger à l'autre, la sérialisation JSON question, que je considère comme exceptionnellement crédible ainsi qu'une source officielle (comme en Python core developer).

portabilité à Python 3

le code ci-dessus ne fonctionne pas comme indiqué dans Python 3 parce que json.dumps() renvoie un bytes objet que le JSONEncoder ne peut pas gérer. Toutefois, cette approche est toujours valable. Une façon simple de contourner le l'édition est à latin1 "décoder" la valeur retournée de pickle.dumps() et puis "l'encoder" 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 et puis encodé à nouveau à la chaîne originale (comme indiqué dans cette réponse par Sven Marnach ).

(Bien que ce qui suit fonctionne bien en Python 2, le latin1 décodant et encodant 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
61
répondu martineau 2018-05-15 15:01:38

vous pouvez étendre la classe DCT 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 régulier, étendre 'sérialisable':

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>>"
}
9
répondu Aravindan Ve 2015-08-04 10:25:29

je suggère de mettre le hack dans la définition de classe. De cette façon, une fois que la classe est définie, elle supporte 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
répondu Yoav Kleinberger 2013-09-06 01:58:42

le problème avec JSONEncoder().default est que vous ne pouvez le faire qu'une seule fois. Si vous tombez sur quelque chose d'un type de données spécial qui ne fonctionne pas avec ce modèle (comme si vous utilisez un étrange encodage). Avec le modèle ci-dessous, vous pouvez toujours rendre votre classe JSON sérialisable, à condition que le champ de classe que vous voulez sérialiser soit lui-même sérialisable (et peut être ajouté à une liste python, à peine n'importe quoi). Sinon, vous devez appliquer récursivement le même modèle de votre champ json (ou extraire les données sérialisables de celui-ci):

# 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
répondu ribamar 2016-02-29 10:55:59

Je ne comprends pas pourquoi vous ne pouvez pas écrire une fonction serialize pour votre propre classe? Vous implémentez l'encodeur personnalisé à l'intérieur de la classe elle-même et permettez à "people" d'appeler la fonction serialize qui retournera essentiellement self.__dict__ avec des fonctions supprimées.

edit:

Cette question d'accord avec moi, que le moyen le plus simple est d'écrire votre propre méthode et retour le json de données sérialisées que vous voulez. Ils nous vous recommandons également d'essayer jsonpickle, mais maintenant vous ajoutez une dépendance supplémentaire pour la beauté lorsque la bonne solution est intégrée.

0
répondu blakev 2017-05-23 11:47:23