Cómo serializar conjuntos JSON?


Tengo un Python set que contiene objetos con métodos __hash__ y __eq__ para asegurarse de que no se incluyan duplicados en la colección.

Necesito codificar json este resultado set, pero pasar incluso un set vacío al método json.dumps genera un TypeError.

  File "/usr/lib/python2.7/json/encoder.py", line 201, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib/python2.7/json/encoder.py", line 264, in iterencode
    return _iterencode(o, 0)
  File "/usr/lib/python2.7/json/encoder.py", line 178, in default
    raise TypeError(repr(o) + " is not JSON serializable")
TypeError: set([]) is not JSON serializable

Sé que puedo crear una extensión para la clase json.JSONEncoder que tiene un método personalizado default, pero ni siquiera estoy seguro de por dónde comenzar a convertir sobre la set. Debo crear un diccionario de la set valores dentro del método predeterminado, y luego devuelve la codificación en eso? Idealmente, me gustaría hacer que el método predeterminado sea capaz de manejar todos los tipos de datos que el codificador original se atraganta (estoy usando Mongo como fuente de datos, por lo que las fechas parecen generar este error también)

Cualquier indicio en la dirección correcta sería apreciado.

EDITAR:

Gracias por la respuesta! Tal vez debería haber sido más preciso.

Utilicé (y voté a favor) las respuestas aquí para obtener alrededor de las limitaciones de la set que se traduce, pero hay claves internas que son un problema también.

Los objetos en el set son objetos complejos que se traducen a __dict__, pero ellos mismos también pueden contener valores para sus propiedades que podrían no ser elegibles para los tipos básicos en el codificador json.

Hay muchos tipos diferentes que entran en este set, y el hash básicamente calcula un id único para la entidad, pero en el verdadero espíritu de NoSQL no hay decir exactamente lo que contiene el objeto hijo.

Un objeto puede contener un valor de fecha para starts, mientras que otro puede tener algún otro esquema que no incluya claves que contengan objetos "no primitivos".

Es por eso que la única solución que se me ocurrió fue extender el JSONEncoder para reemplazar el método default para activar diferentes casos, pero no estoy seguro de cómo hacerlo y la documentación es ambigua. En objetos anidados, ¿el valor devuelto desde default va por clave, o ¿es solo un include/descarte genérico que mira todo el objeto? ¿Cómo se adapta ese método a los valores anidados? He mirado a través de preguntas anteriores y parece que no puedo encontrar el mejor enfoque para la codificación de casos específicos (que desafortunadamente parece lo que voy a tener que hacer aquí).

Author: martineau, 2011-11-22

5 answers

JSON la notación tiene solo un puñado de tipos de datos nativos (objetos, matrices, cadenas, números, booleanos y null), por lo que cualquier cosa serializada en JSON debe expresarse como uno de estos tipos.

Como se muestra en el json module docs , esta conversión se puede hacer automáticamente por un JSONEncoder y JSONDecoder , pero entonces estaría renunciando a alguna otra estructura que pueda necesitar (si convierte conjuntos en una lista, entonces perderá la capacidad de recuperar listas regulares; si convierte conjuntos a un diccionario usando dict.fromkeys(s) entonces pierde la capacidad de recuperar diccionarios).

Una solución más sofisticada es crear un tipo personalizado que pueda coexistir con otros tipos JSON nativos. Esto le permite almacenar estructuras anidadas que incluyen listas, conjuntos,dictados, decimales, objetos datetime, etc.:

from json import dumps, loads, JSONEncoder, JSONDecoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, unicode, int, float, bool, type(None))):
            return JSONEncoder.default(self, obj)
        return {'_python_object': pickle.dumps(obj)}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(str(dct['_python_object']))
    return dct

Aquí hay una sesión de ejemplo que muestra que puede manejar listas, dictados y conjuntos:

>>> data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]

>>> j = dumps(data, cls=PythonObjectEncoder)

>>> loads(j, object_hook=as_python_object)
[1, 2, 3, set(['knights', 'say', 'who', 'ni']), {u'key': u'value'}, Decimal('3.14')]

Alternativamente, puede ser útil utilizar un más técnica de serialización de propósito general como YAML, Twisted Jelly , o el módulo de pepinillos de Python . Cada uno de estos soporta un rango mucho mayor de tipos de datos.

 86
Author: Raymond Hettinger,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-02 07:22:14

Puede crear un codificador personalizado que devuelva un list cuando encuentre un set. He aquí un ejemplo:

>>> import json
>>> class SetEncoder(json.JSONEncoder):
...    def default(self, obj):
...       if isinstance(obj, set):
...          return list(obj)
...       return json.JSONEncoder.default(self, obj)
... 
>>> json.dumps(set([1,2,3,4,5]), cls=SetEncoder)
'[1, 2, 3, 4, 5]'

También puede detectar otros tipos de esta manera. Si necesita conservar que la lista es realmente un conjunto, puede usar una codificación personalizada. Algo como return {'type':'set', 'list':list(obj)} podría funcionar.

Para los tipos anidados ilustrados, considere serializar esto:

>>> class Something(object):
...    pass
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)

Esto plantea el siguiente error:

TypeError: <__main__.Something object at 0x1691c50> is not JSON serializable

Esto indica que el codificador tomará el resultado list devuelto y recursivamente llame al serializador en sus hijos. Para agregar un serializador personalizado para varios tipos, puede hacer esto:

>>> class SetEncoder(json.JSONEncoder):
...    def default(self, obj):
...       if isinstance(obj, set):
...          return list(obj)
...       if isinstance(obj, Something):
...          return 'CustomSomethingRepresentation'
...       return json.JSONEncoder.default(self, obj)
... 
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)
'[1, 2, 3, 4, 5, "CustomSomethingRepresentation"]'
 78
Author: jterrace,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-23 04:30:47

Solo los diccionarios, Listas y tipos de objetos primitivos (int, string, bool) están disponibles en JSON.

 4
Author: Joseph Le Brech,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-22 16:42:30

Si solo necesita codificar conjuntos, no objetos Python generales, y desea mantenerlo fácilmente legible por humanos, se puede usar una versión simplificada de la respuesta de Raymond Hettinger:

import json
import collections

class JSONSetEncoder(json.JSONEncoder):
    """Use with json.dumps to allow Python sets to be encoded to JSON

    Example
    -------

    import json

    data = dict(aset=set([1,2,3]))

    encoded = json.dumps(data, cls=JSONSetEncoder)
    decoded = json.loads(encoded, object_hook=json_as_python_set)
    assert data == decoded     # Should assert successfully

    Any object that is matched by isinstance(obj, collections.Set) will
    be encoded, but the decoded value will always be a normal Python set.

    """

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        else:
            return json.JSONEncoder.default(self, obj)

def json_as_python_set(dct):
    """Decode json {'_set_object': [1,2,3]} to set([1,2,3])

    Example
    -------
    decoded = json.loads(encoded, object_hook=json_as_python_set)

    Also see :class:`JSONSetEncoder`

    """
    if '_set_object' in dct:
        return set(dct['_set_object'])
    return dct
 3
Author: NeilenMarais,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-02-05 08:37:20

Adaptéla solución de Raymond Hettinger a python 3.

Esto es lo que ha cambiado:

  • unicode desaparecido
  • se actualizó la llamada a los padres default con super()
  • usando base64 para serializar el tipo bytes en str (porque parece que bytes en python 3 no se puede convertir a JSON)
from decimal import Decimal
from base64 import b64encode, b64decode
from json import dumps, loads, JSONEncoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (list, dict, str, int, float, bool, type(None))):
            return super().default(obj)
        return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(b64decode(dct['_python_object'].encode('utf-8')))
    return dct

data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]
j = dumps(data, cls=PythonObjectEncoder)
print(loads(j, object_hook=as_python_object))
# prints: [1, 2, 3, {'knights', 'who', 'say', 'ni'}, {'key': 'value'}, Decimal('3.14')]
 3
Author: simlmx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:18:24