Como JSON serializar conjuntos?

148

Eu tenho um Python setque contém objetos __hash__e __eq__métodos para garantir que não haja duplicatas incluídas na coleção.

Eu preciso json codificar esse resultado set, mas passar mesmo um vazio setpara o json.dumpsmétodo gera a 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

Eu sei que posso criar uma extensão para a json.JSONEncoderclasse que possui um defaultmétodo personalizado , mas nem sei por onde começar a conversão pelo set. Devo criar um dicionário com os setvalores dentro do método padrão e depois retornar a codificação? Idealmente, gostaria de tornar o método padrão capaz de lidar com todos os tipos de dados que o codificador original usa (estou usando o Mongo como fonte de dados, para que as datas também pareçam causar esse erro)

Qualquer dica na direção certa seria apreciada.

EDITAR:

Obrigado pela resposta! Talvez eu devesse ter sido mais preciso.

Utilizei (e votei) as respostas aqui para contornar as limitações da settradução, mas há chaves internas que também são um problema.

Os objetos no setsão objetos complexos aos quais se traduzem __dict__, mas eles também podem conter valores para suas propriedades que podem ser inelegíveis para os tipos básicos no codificador json.

Existem muitos tipos diferentes para isso set, e o hash calcula basicamente um ID exclusivo para a entidade, mas no verdadeiro espírito do NoSQL não há como dizer exatamente o que o objeto filho contém.

Um objeto pode conter um valor de data para starts, enquanto outro pode ter outro esquema que não inclui chaves que contenham objetos "não primitivos".

É por isso que a única solução em que consegui pensar foi estender o método JSONEncoderpara substituir o defaultmétodo para ativar casos diferentes - mas não sei ao certo como proceder e a documentação é ambígua. Em objetos aninhados, o valor retornado de defaultpassa por chave ou é apenas uma inclusão / descarte genérica que examina o objeto inteiro? Como esse método acomoda valores aninhados? Examinei as perguntas anteriores e não consigo encontrar a melhor abordagem para a codificação específica de caso (que infelizmente parece ser o que vou precisar fazer aqui).

DeaconDesperado
fonte
3
por que dicts? Eu acho que você quer fazer apenas um listfora do set e, em seguida, passá-lo para o codificador ... por exemplo:encode(list(myset))
Constantinius
2
Em vez de usar JSON, você poderia usar YAML (JSON é essencialmente um subconjunto de YAML).
Paolo Moretti
@PaoloMoretti: Mas traz alguma vantagem? Não acho que os conjuntos estejam entre os tipos de dados universalmente suportados do YAML e são menos amplamente suportados, principalmente em relação às APIs.
@PaoloMoretti Obrigado pela sua contribuição, mas o frontend do aplicativo requer JSON como um tipo de retorno e esse requisito é para todos os fins corrigido.
DeaconDesperado
2
@ delnan Eu estava sugerindo YAML porque ele tem um suporte nativo para conjuntos e datas .
Paolo Moretti

Respostas:

116

A notação JSON possui apenas alguns tipos de dados nativos (objetos, matrizes, seqüências de caracteres, números, booleanos e nulos); portanto, qualquer coisa serializada no JSON precisa ser expressa como um desses tipos.

Conforme mostrado nos documentos do módulo json , essa conversão pode ser feita automaticamente por JSONEncoder e JSONDecoder , mas você desistiria de outra estrutura necessária (se converter conjuntos em uma lista, perderá a capacidade de recuperar regularmente se você converter conjuntos para um dicionário usandodict.fromkeys(s) , perderá a capacidade de recuperar dicionários).

Uma solução mais sofisticada é criar um tipo personalizado que possa coexistir com outros tipos JSON nativos. Isso permite que você armazene estruturas aninhadas que incluem listas, conjuntos, dicts, decimais, objetos de data e 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

Aqui está uma sessão de amostra mostrando que ele pode lidar com listas, dictos e 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')]

Como alternativa, pode ser útil usar uma técnica de serialização de uso mais geral, como YAML , Twisted Jelly ou módulo de pickle do Python . Cada um deles suporta uma variedade muito maior de tipos de dados.

Raymond Hettinger
fonte
11
Esta é a primeira vez que ouço que YAML é o propósito mais geral do que JSON ... o_O
Karl Knechtel
13
@KarlKnechtel YAML é um superconjunto do JSON (muito próximo). Ele também adiciona tags para dados binários, conjuntos, mapas ordenados e registros de data e hora. Apoiar mais tipos de dados é o que eu quis dizer com "propósito mais geral". Você parece estar usando a frase "objetivo geral" em um sentido diferente.
Raymond Hettinger
4
Não esqueça também do jsonpickle , que se destina a ser uma biblioteca generalizada para selecionar objetos Python para JSON, da mesma forma que esta resposta sugere.
Jason R. Coombs
4
A partir da versão 1.2, o YAML é um superconjunto estrito do JSON. Todo JSON legal agora é YAML legal. yaml.org/spec/1.2/spec.html
steveha 16/10
2
este código de exemplo importa JSONDecoderfaz, mas não usá-lo
watsonic
115

Você pode criar um codificador personalizado que retorne a listquando encontrar a set. Aqui está um exemplo:

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

Você também pode detectar outros tipos dessa maneira. Se você precisar manter a lista como um conjunto, poderá usar uma codificação personalizada. Algo comoreturn {'type':'set', 'list':list(obj)} pode funcionar.

Para ilustrar tipos aninhados, considere serializar isso:

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

Isso gera o seguinte erro:

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

Isso indica que o codificador receberá o listresultado retornado e chamará recursivamente o serializador em seus filhos. Para adicionar um serializador personalizado para vários tipos, você pode fazer isso:

>>> 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
fonte
Obrigado, editei a pergunta para especificar melhor que esse era o tipo de coisa que eu precisava. O que não consigo entender é como esse método manipula objetos aninhados. No seu exemplo, o valor de retorno é lista para conjunto, mas e se o objeto passado fosse um conjunto com datas (outro tipo de dados inválido) dentro dele? Devo detalhar as chaves no próprio método padrão? Muito obrigado!
DeaconDesperado
1
Eu acho que o módulo JSON lida com objetos aninhados para você. Quando a lista voltar, ele irá percorrer os itens da lista que tentam codificar cada um. Se um deles for uma data, a defaultfunção será chamada novamente, desta vez objsendo um objeto de data, então você só precisa testá-lo e retornar uma representação de data.
jterrace
Portanto, é possível que o método padrão possa ser executado várias vezes para qualquer objeto passado, pois também examinará as chaves individuais depois de "listadas"?
DeaconDesperado
Mais ou menos, ele não será chamado várias vezes para o mesmo objeto, mas poderá recorrer para filhos. Veja a resposta atualizada.
jterrace
Funcionou exatamente como você descreveu. Eu ainda tenho que descobrir algumas das falhas, mas a maioria é provavelmente algo que pode ser refatorado. Muito obrigado pela sua orientação!
DeaconDesperado
7

I adaptado solução de Raymond Hettinger para pitão 3.

Aqui está o que mudou:

  • unicode desaparecido
  • atualizou a chamada para os pais defaultcomsuper()
  • usando base64para serializar o bytestipo em str(porque parece que bytesno python 3 não pode ser convertido em 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
fonte
4
O código mostrado no final desta resposta para uma pergunta relacionada realiza a mesma coisa [apenas] decodificando e codificando o objeto de bytes json.dumps()retorna de / para 'latin1', pulando o base64que não é necessário.
martineau
6

Apenas dicionários, listas e tipos de objetos primitivos (int, string, bool) estão disponíveis no JSON.

Joseph Le Brech
fonte
5
"Tipo de objeto primitivo" não faz sentido quando se fala em Python. "Objeto embutido" faz mais sentido, mas é muito amplo aqui (para iniciantes: inclui ditados, listas e também conjuntos). (A terminologia JSON pode ser diferente.)
string número objeto array true false null
Joseph Le Brech 22/11
6

Você não precisa criar uma classe de codificador personalizada para fornecer o defaultmétodo - ele pode ser passado como um argumento de palavra-chave:

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)

resulta em [1, 2, 3]todas as versões suportadas do Python.

Antti Haapala
fonte
4

Se você precisar codificar apenas conjuntos, não objetos gerais do Python, e quiser mantê-lo facilmente legível por humanos, uma versão simplificada da resposta de Raymond Hettinger poderá ser usada:

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
fonte
1

Se você precisar de um despejo rápido e não quiser implementar um codificador personalizado. Você pode usar o seguinte:

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

Isso converterá todos os conjuntos (e outras iteráveis) em matrizes. Apenas tome cuidado para que esses campos permaneçam matrizes quando você analisa o json de volta. Se você deseja preservar os tipos, precisa escrever um codificador personalizado.

David Novák
fonte
7
Quando tento isso, obtenho: TypeError: __init __ () obteve um argumento inesperado de palavra-chave 'iterable_as_array'
atm
Você precisa instalar o simplejson #
11449 JerryBringer
importação simplejson como json e, em seguida, json_string = json.dumps (dados, iterable_as_array = true) funciona bem em Python 3,6
fraverta
1

Uma falha da solução aceita é que sua saída é muito específica para python. Ou seja, sua saída json bruta não pode ser observada por um ser humano ou carregada por outro idioma (por exemplo, javascript). exemplo:

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

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

Você receberá:

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

Posso propor uma solução que faça o downgrade do conjunto para um ditado que contém uma lista na saída e volte para um conjunto quando carregado no python usando o mesmo codificador, preservando, portanto, a observabilidade e o agnosticismo da linguagem:

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

O que você recebe:

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

Observe que a serialização de um dicionário que possui um elemento com uma chave "__set__"interromperá esse mecanismo. Então __set__agora se tornou uma dictchave reservada . Obviamente, sinta-se à vontade para usar outra tecla mais ofuscada.

sagismo
fonte