¿Cómo JSON serializar conjuntos?

148

Tengo un Python setque contiene objetos con__hash__ y __eq__métodos para asegurarse de que no se incluyan duplicados en la colección.

Necesito codificar json este resultado set, pero pasar incluso un vacío setal json.dumpsmétodo 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 de la json.JSONEncoderclase que tenga un defaultmétodo personalizado , pero ni siquiera estoy seguro de por dónde empezar a convertir set. ¿Debo crear un diccionario a partir de los setvalores dentro del método predeterminado y luego devolver la codificación? Idealmente, me gustaría hacer que el método predeterminado sea capaz de manejar todos los tipos de datos en los que el codificador original se atraganta (estoy usando Mongo como fuente de datos, por lo que las fechas también parecen generar este error)

Cualquier sugerencia en la dirección correcta sería apreciada.

EDITAR:

¡Gracias por la respuesta! Quizás debería haber sido más preciso.

Utilicé (y voté) las respuestas aquí para sortear las limitaciones de la set traducción, pero también hay claves internas que son un problema.

Los objetos en el setson objetos complejos que se traducen en__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 entrando en esto set , y el hash básicamente calcula una identificación única para la entidad, pero en el verdadero espíritu de NoSQL no se sabe exactamente qué 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 JSONEncoderpara reemplazar el defaultmétodo para activar diferentes casos, pero no estoy seguro de cómo hacerlo y la documentación es ambigua. En los objetos anidados, ¿el valor devuelto por defaultir por clave, o es solo una inclusión / descarte genérico que mira todo el objeto? ¿Cómo acomoda ese método los valores anidados? He revisado las preguntas anteriores y parece que no puedo encontrar el mejor enfoque para la codificación de casos específicos (que desafortunadamente parece ser lo que voy a necesitar hacer aquí).

DiáconoDesperado
fuente
3
por qué dicts? Creo que solo quieres hacer un listout del set y luego pasarlo al codificador ... por ejemplo:encode(list(myset))
Constantinius
2
En lugar de usar JSON, podría usar YAML (JSON es esencialmente un subconjunto de YAML).
Paolo Moretti
@PaoloMoretti: ¿Sin embargo, trae alguna ventaja? No creo que los conjuntos se encuentren entre los tipos de datos de YAML con soporte universal, y es menos compatible, especialmente con respecto a las API.
@PaoloMoretti Gracias por su aporte, pero la interfaz de la aplicación requiere JSON como tipo de retorno y este requisito es fijo para todos los propósitos.
DeaconDesperado
2
@delnan Estaba sugiriendo YAML porque tiene un soporte nativo para conjuntos y fechas .
Paolo Moretti

Respuestas:

116

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

Como se muestra en los documentos del módulo json , esta conversión se puede hacer automáticamente mediante un JSONEncoder y JSONDecoder , pero luego estaría renunciando a alguna otra estructura que pueda necesitar (si convierte conjuntos en una lista, entonces pierde la capacidad de recuperar regularmente listas; si convierte conjuntos a un diccionario utilizando, dict.fromkeys(s)entonces pierde la capacidad de recuperar diccionarios).

Una solución más sofisticada es construir un tipo personalizado que pueda coexistir con otros tipos JSON nativos. Esto le permite almacenar estructuras anidadas que incluyen listas, conjuntos, dictados, decimales, objetos de fecha y hora, 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 muestra 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 una técnica de serialización de propósito más general como YAML , Twisted Jelly o el módulo de pepinillos de Python . Cada uno de ellos admite una gama mucho mayor de tipos de datos.

Raymond Hettinger
fuente
11
Esta es la primera vez que escucho que YAML tiene un propósito más general que JSON ... o_O
Karl Knechtel
13
@KarlKnechtel YAML es un superconjunto de JSON (casi). También agrega etiquetas para datos binarios, conjuntos, mapas ordenados y marcas de tiempo. Apoyar más tipos de datos es lo que quise decir con "propósito más general". Parece que estás usando la frase "propósito general" en un sentido diferente.
Raymond Hettinger
44
No olvide también jsonpickle , que está destinado a ser una biblioteca generalizada para encurtir objetos Python en JSON, como sugiere esta respuesta.
Jason R. Coombs
44
A partir de la versión 1.2, YAML es un superconjunto estricto de JSON. Todo JSON legal ahora es YAML legal. yaml.org/spec/1.2/spec.html
steveha
2
este ejemplo de código importa JSONDecoderpero no lo usa
watsonic
115

Puede crear un codificador personalizado que devuelva a listcuando encuentre a set. Aquí hay 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]'

Puede detectar otros tipos de esta manera también. Si necesita conservar que la lista era en realidad un conjunto, podría usar una codificación personalizada. Algo comoreturn {'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 genera el siguiente error:

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

Esto indica que el codificador tomará el listresultado devuelto y llamará recursivamente 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"]'
jterrace
fuente
Gracias, edité la pregunta para especificar mejor que este era el tipo de cosas que necesitaba. Lo que parece que no puedo entender es cómo este método manejará los objetos anidados. En su ejemplo, el valor de retorno es una lista para el conjunto, pero ¿qué sucede si el objeto pasado fue un conjunto con fechas (otro tipo de datos incorrecto) dentro de él? ¿Debería profundizar en las claves dentro del método predeterminado? ¡Gracias una tonelada!
DeaconDesperado
1
Creo que el módulo JSON maneja objetos anidados para usted. Una vez que recupere la lista, iterará sobre los elementos de la lista tratando de codificar cada uno. Si uno de ellos es una fecha, la defaultfunción volverá a llamarse, esta vez por objser un objeto de fecha, por lo que solo debe probarlo y devolver una representación de fecha.
jterrace
Entonces, ¿el método predeterminado podría ejecutarse varias veces para cualquier objeto que se le pase, ya que también examinará las claves individuales una vez que esté "enumerado"?
DeaconDesperado
Más o menos, no se llamará varias veces para el mismo objeto, pero puede recurrir a los niños. Ver respuesta actualizada.
jterrace
Funcionó exactamente como lo describiste. Todavía tengo que resolver algunas de las fallas, pero la mayoría de ellas son probablemente cosas que se pueden refactorizar. ¡Muchas gracias por tu orientación!
DeaconDesperado
7

Adapté solución de Raymond Hettinger a Python 3.

Esto es lo que ha cambiado:

  • unicode desaparecido
  • actualizado la llamada a los padres defaultconsuper()
  • usando base64para serializar el bytestipo en str(porque parece que bytesen 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')]
simlmx
fuente
44
El código que se muestra al final de esta respuesta a una pregunta relacionada logra lo mismo al [solo] decodificar y codificar los json.dumps()retornos del objeto a / desde 'latin1', omitiendo las base64cosas que no son necesarias.
Martineau
6

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

Joseph Le Brech
fuente
55
El "tipo de objeto primitivo" no tiene sentido cuando se habla de Python. El "objeto incorporado" tiene más sentido, pero aquí es demasiado amplio (para empezar: incluye dictados, listas y también conjuntos). (Sin embargo, la terminología de JSON puede ser diferente)
matriz de objetos de número de cadena verdadero falso nulo
Joseph Le Brech
6

No necesita crear una clase de codificador personalizada para proporcionar el defaultmétodo; se puede pasar como un argumento de palabra clave:

import json

def serialize_sets(obj):
    if isinstance(obj, set):
        return list(obj)

    return obj

json_str = json.dumps(set([1,2,3]), default=serialize_sets)
print(json_str)

da como resultado [1, 2, 3]todas las versiones compatibles de Python.

Antti Haapala
fuente
4

Si solo necesita codificar conjuntos, no objetos generales de Python, y desea mantenerlo fácilmente legible para los 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
NeilenMarais
fuente
1

Si solo necesita un volcado rápido y no desea implementar un codificador personalizado. Puedes usar lo siguiente:

json_string = json.dumps(data, iterable_as_array=True)

Esto convertirá todos los conjuntos (y otros iterables) en matrices. Solo tenga en cuenta que esos campos permanecerán como matrices cuando analice el json. Si desea conservar los tipos, debe escribir un codificador personalizado.

David Novák
fuente
77
Cuando intento esto obtengo: TypeError: __init __ () obtuvo un argumento de palabra clave inesperado 'iterable_as_array'
atm
Necesitas instalar simplejson
JerryBringer
importar simplejson como json y luego json_string = json.dumps (data, iterable_as_array = True) funciona bien en Python 3.6
fraverta
1

Una deficiencia de la solución aceptada es que su salida es muy específica de Python. Es decir, su salida json sin procesar no puede ser observada por un humano o cargada por otro idioma (por ejemplo, javascript). ejemplo:

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)

Te conseguirá:

{"a": [44, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsESwVLBmWFcQJScQMu"}], "b": [55, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsCSwNLBGWFcQJScQMu"}]}

Puedo proponer una solución que rebaja el conjunto a un dict que contiene una lista al salir y vuelve a un conjunto cuando se carga en python usando el mismo codificador, preservando así la observabilidad y el agnosticismo del lenguaje:

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)
        elif isinstance(obj, set):
            return {"__set__": list(obj)}
        return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}

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

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)
ob = loads(j)
print(ob["a"])

Lo que te lleva a:

{"a": [44, {"__set__": [4, 5, 6]}], "b": [55, {"__set__": [2, 3, 4]}]}
[44, {'__set__': [4, 5, 6]}]

Tenga en cuenta que serializar un diccionario que tiene un elemento con una clave "__set__"romperá este mecanismo. Entonces se __set__ha convertido en una dictclave reservada . Obviamente, siéntase libre de usar otra clave más profundamente ofuscada.

sagismo
fuente