Hacer que el objeto JSON sea serializable con un codificador normal

Resuelto leonsas asked hace 11 años • 6 respuestas

La forma habitual de serializar JSON objetos personalizados no serializables es crear subclases json.JSONEncodery luego pasar un codificador personalizado a json.dumps().

Suele tener este aspecto:

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

Lo que intento hacer es hacer algo serializable con el codificador predeterminado. Miré a mi alrededor pero no pude encontrar nada. Mi idea es que habría algún campo en el que el codificador mira para determinar la codificación json. Algo parecido a __str__. Quizás un __json__campo. ¿Existe algo como esto en Python?

Quiero que una clase de un módulo que estoy creando sea serializable en JSON para todos los que usan el paquete sin que se preocupen por implementar sus propios codificadores personalizados [triviales].

leonsas avatar Aug 28 '13 09:08 leonsas
Aceptado

Como dije en un comentario a tu pregunta, después de mirar el jsoncódigo fuente del módulo, no parece prestarse a hacer lo que deseas. Sin embargo, el objetivo podría lograrse mediante lo que se conoce como parche de mono (consulte la pregunta ¿Qué es un parche de mono? ). Esto podría hacerse en __init__.pyel script de inicialización de su paquete y afectaría toda jsonla serialización posterior del módulo, ya que los módulos generalmente solo se cargan una vez y el resultado se almacena en caché sys.modules.

El parche cambia el método del codificador json predeterminado default: el default().

A continuación se muestra un ejemplo implementado como un módulo independiente en aras de la simplicidad:

Módulo: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.

Usarlo es trivial ya que el parche se aplica simplemente importando el módulo.

Script de cliente de muestra:

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

Para conservar la información del tipo de objeto, el método especial también puede incluirla en la cadena devuelta:

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

Lo que produce el siguiente JSON que ahora incluye el nombre de la clase:

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

La magia yace aquí

Incluso mejor que hacer que el reemplazo default()busque un método con un nombre especial sería que pudiera serializar la mayoría de los objetos de Python automáticamente , incluidas las instancias de clases definidas por el usuario, sin necesidad de agregar un método especial. Después de investigar una serie de alternativas, la siguiente, basada en una respuesta de @Raymond Hettinger a otra pregunta, que utiliza el picklemódulo, me pareció la más cercana a ese ideal:

Módulo: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.

Por supuesto, no todo se puede conservar en vinagre, por ejemplo, los tipos de extensiones. Sin embargo, hay formas definidas de manejarlos a través del protocolo pickle escribiendo métodos especiales, similares a lo que usted sugirió y que describí anteriormente, pero hacerlo probablemente sería necesario para un número mucho menor de casos.

Deserializar

De todos modos, usar el protocolo pickle también significa que sería bastante fácil reconstruir el objeto Python original proporcionando un object_hookargumento de función personalizado en cualquier json.loads()llamada que use cualquier '_python_object'clave del diccionario pasada, siempre que tenga una. Algo como:

def as_python_object(dct):
    try:
        return pickle.loads(str(dct['_python_object']))
    except KeyError:
        return dct

pyobj = json.loads(json_str, object_hook=as_python_object)

Si esto tiene que hacerse en muchos lugares, podría valer la pena definir una función contenedora que proporcione automáticamente el argumento de palabra clave adicional:

json_pkloads = functools.partial(json.loads, object_hook=as_python_object)

pyobj = json_pkloads(json_str)

jsonNaturalmente, esto también podría parchearse en el módulo, haciendo que la función sea la predeterminada object_hook(en lugar de None).

Se me ocurrió la idea de usarlo picklea partir de una respuesta de Raymond Hettinger a otra pregunta sobre serialización de JSON, a quien considero excepcionalmente creíble y también una fuente oficial (como en el desarrollador principal de Python).

Portabilidad a Python 3

El código anterior no funciona como se muestra en Python 3 porque json.dumps()devuelve un bytesobjeto que JSONEncoderno puede manejar. Sin embargo, el enfoque sigue siendo válido. Una forma sencilla de solucionar el problema es latin1"decodificar" el valor devuelto pickle.dumps()y luego "codificarlo" latin1antes de pasarlo a pickle.loads()la as_python_object()función. Esto funciona porque las cadenas binarias arbitrarias son válidas latin1y siempre se pueden decodificar en Unicode y luego volver a codificarlas en la cadena original (como lo señala Sven Marnach en esta respuesta ).

(Aunque lo siguiente funciona bien en Python 2, la latin1decodificación y codificación que realiza es superflua).

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):
    try:
        return pickle.loads(dct['_python_object'].encode('latin1'))
    except KeyError:
        return dct


class Foo(object):  # Some user-defined class.
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        if type(other) is type(self):  # Instances of same class?
            return self.name == other.name
        return NotImplemented

    __hash__ = None


data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'},
        Foo('Bar'), Decimal('3.141592653589793238462643383279502884197169')]
j = json.dumps(data, cls=PythonObjectEncoder, indent=4)
data2 = json.loads(j, object_hook=as_python_object)
assert data == data2  # both should be same
martineau avatar Sep 01 '2013 17:09 martineau

Puedes extender la clase dict así:

#!/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())

Ahora, para que tus clases sean serializables con el codificador normal, extiende 'Serializable':

class MySerializableClass(Serializable):

    attr_1 = 'first attribute'
    attr_2 = 23

    def my_function(self):
        print('do something here')


obj = MySerializableClass()

print(obj)imprimirá algo como:

<__main__.MySerializableClass object at 0x1073525e8>

print(json.dumps(obj, indent=4))imprimirá algo como:

{
    "attr_1": "first attribute",
    "attr_2": 23,
    "my_function": "<bound method MySerializableClass.my_function of <__main__.MySerializableClass object at 0x1073525e8>>"
}
aravindanve avatar Jul 31 '2015 09:07 aravindanve