Come serializzare i set JSON?


149

Ho un Python setche contiene oggetti __hash__e __eq__metodi per assicurarsi che nessun duplicato sia incluso nella raccolta.

Ho bisogno di json codificare questo risultato set, ma passare anche un vuoto setal json.dumpsmetodo 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

So che posso creare un'estensione per la json.JSONEncoderclasse che ha un defaultmetodo personalizzato , ma non sono nemmeno sicuro da dove iniziare la conversione su set. Devo creare un dizionario dai setvalori all'interno del metodo predefinito e quindi restituire la codifica su quello? Idealmente, vorrei rendere il metodo predefinito in grado di gestire tutti i tipi di dati su cui l'encoder originale soffoca (sto usando Mongo come fonte di dati, quindi le date sembrano sollevare anche questo errore)

Qualsiasi suggerimento nella giusta direzione sarebbe apprezzato.

MODIFICARE:

Grazie per la risposta! Forse avrei dovuto essere più preciso.

Ho usato (e valutato) le risposte qui per aggirare i limiti della settraduzione, ma ci sono anche chiavi interne che sono un problema.

Gli oggetti nel setsono oggetti complessi che si traducono in __dict__, ma essi stessi possono anche contenere valori per le loro proprietà che potrebbero non essere idonei per i tipi di base nel codificatore json.

Ci sono molti tipi diversi in questo set, e l'hash calcola fondamentalmente un ID univoco per l'entità, ma nel vero spirito di NoSQL non si può dire esattamente cosa contiene l'oggetto figlio.

Un oggetto potrebbe contenere un valore di data per starts, mentre un altro potrebbe avere qualche altro schema che non include chiavi contenenti oggetti "non primitivi".

Questo è il motivo per cui l'unica soluzione che mi è venuta in mente è stata quella di estendere la JSONEncodersostituzione del defaultmetodo per attivare casi diversi, ma non sono sicuro di come procedere e la documentazione è ambigua. Negli oggetti nidificati, il valore restituito da defaultva per chiave o è solo un generico include / discard che esamina l'intero oggetto? In che modo quel metodo accetta valori nidificati? Ho esaminato le domande precedenti e non riesco a trovare l'approccio migliore alla codifica specifica del caso (che sfortunatamente sembra ciò che dovrò fare qui).


3
perché dicts? Penso che tu voglia fare solo un listset dal set e poi passarlo all'encoder ... ad esempio:encode(list(myset))
Costantino

2
Invece di utilizzare JSON, è possibile utilizzare YAML (JSON è essenzialmente un sottoinsieme di YAML).
Paolo Moretti,

@PaoloMoretti: porta qualche vantaggio però? Non penso che i set siano tra i tipi di dati universalmente supportati di YAML, ed è meno ampiamente supportato, soprattutto per quanto riguarda le API.

@PaoloMoretti Grazie per il tuo contributo, ma il frontend dell'applicazione richiede JSON come tipo di ritorno e questo requisito è fisso a tutti gli effetti.
DeaconDesperado,

2
@delnan Stavo suggerendo YAML perché ha un supporto nativo sia per i set che per le date .
Paolo Moretti,

Risposte:


117

La notazione JSON ha solo una manciata di tipi di dati nativi (oggetti, matrici, stringhe, numeri, valori booleani e null), quindi qualsiasi cosa serializzata in JSON deve essere espressa come uno di questi tipi.

Come mostrato nei documenti del modulo json , questa conversione può essere eseguita automaticamente da un JSONEncoder e JSONDecoder , ma poi rinunceresti a qualche altra struttura di cui potresti aver bisogno (se converti i set in un elenco, perdi la possibilità di recuperare regolarmente elenchi; se si convertono set in un dizionario utilizzando, dict.fromkeys(s)si perde la possibilità di recuperare dizionari).

Una soluzione più sofisticata è quella di creare un tipo personalizzato che può coesistere con altri tipi JSON nativi. Ciò consente di memorizzare strutture nidificate che includono elenchi, set, dadi, decimali, oggetti datetime, ecc .:

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

Ecco una sessione di esempio che mostra che è in grado di gestire elenchi, dicts e set:

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

In alternativa, può essere utile utilizzare una tecnica di serializzazione più generica come YAML , Twisted Jelly o il modulo pickle di Python . Ciascuno di essi supporta una gamma molto più ampia di tipi di dati.


11
Questo è il primo che ho sentito dire che YAML è più generico di JSON ... o_O
Karl Knechtel,

13
@KarlKnechtel YAML è un superset di JSON (quasi). Aggiunge inoltre tag per dati binari, set, mappe ordinate e timestamp. Supportare più tipi di dati è ciò che intendevo per "scopo più generale". Sembra che tu stia usando la frase "scopo generale" in un senso diverso.
Raymond Hettinger,

4
Non dimenticare anche jsonpickle , che intende essere una libreria generalizzata per decapare gli oggetti Python su JSON, proprio come suggerisce questa risposta.
Jason R. Coombs,

4
A partire dalla versione 1.2, YAML è un superset rigoroso di JSON. Tutti i JSON legali ora sono YAML legali. yaml.org/spec/1.2/spec.html
steveha,

2
questo esempio di codice importa JSONDecoderma non lo usa
watsonic il

115

È possibile creare un codificatore personalizzato che restituisce a listquando incontra a set. Ecco un esempio:

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

Puoi rilevare anche altri tipi in questo modo. Se è necessario mantenere che l'elenco era effettivamente un set, è possibile utilizzare una codifica personalizzata. Qualcosa del genere return {'type':'set', 'list':list(obj)}potrebbe funzionare.

Per i tipi nidificati illustrati, considerare la serializzazione di questo:

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

Ciò genera il seguente errore:

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

Questo indica che l'encoder prenderà il listrisultato restituito e chiamerà ricorsivamente il serializzatore sui suoi figli. Per aggiungere un serializzatore personalizzato per più tipi, è possibile effettuare ciò:

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

Grazie, ho modificato la domanda per specificare meglio che questo era il tipo di cosa di cui avevo bisogno. Quello che non riesco a capire è come questo metodo gestirà gli oggetti nidificati. Nel tuo esempio il valore restituito è list for set, ma cosa succede se l'oggetto passato era un set con date (un altro tipo di dati non valido) al suo interno? Devo eseguire il drill-through delle chiavi all'interno del metodo predefinito stesso? Grazie mille!
DeaconDesperado,

1
Penso che il modulo JSON gestisca oggetti nidificati per te. Una volta ripristinato l'elenco, scorrerà gli elementi dell'elenco tentando di codificarli. Se uno di questi è una data, la defaultfunzione verrà richiamata di nuovo, questa volta objessendo un oggetto data, quindi devi solo provarlo e restituire una rappresentazione della data.
jterrace,

Quindi il metodo predefinito potrebbe plausibilmente essere eseguito più volte per ogni singolo oggetto passato ad esso, dal momento che guarderà anche le singole chiavi una volta che è "elencato"?
DeaconDesperado,

In un certo senso, non verrà chiamato più volte per lo stesso oggetto, ma può ricorrere ai bambini. Vedi la risposta aggiornata.
jterrace,

Ha funzionato esattamente come hai descritto. Devo ancora capire alcuni dei difetti, ma la maggior parte è probabilmente roba che può essere riformulata. Grazie mille per la tua guida!
DeaconDesperado,

7

Ho adattato la soluzione di Raymond Hettinger a Python 3.

Ecco cosa è cambiato:

  • unicode scomparso
  • aggiornata la chiamata a quella dei genitori defaultconsuper()
  • usando base64per serializzare il bytestipo in str(perché sembra che bytesin Python 3 non possa essere convertito in 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')]

4
Il codice mostrato alla fine di questa risposta a una domanda correlata realizza la stessa cosa decodificando e codificando solo l'oggetto da cui l'oggetto json.dumps()ritorna / 'latin1'salta, saltando ciò base64che non è necessario.
martineau,

6

In JSON sono disponibili solo dizionari, elenchi e tipi di oggetti primitivi (int, string, bool).


5
"Tipo di oggetto primitivo" non ha senso quando si parla di Python. "Oggetto incorporato" ha più senso, ma qui è troppo ampio (per cominciare: include dicts, elenchi e anche set). (La terminologia JSON può essere diversa però.)

stringa numero oggetto array vero falso null
Joseph Le Brech,

6

Non è necessario creare una classe di codificatore personalizzata per fornire il defaultmetodo: può essere passato come argomento di parola chiave:

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)

risulta in [1, 2, 3]tutte le versioni di Python supportate.


4

Se hai solo bisogno di codificare set, non oggetti Python generici, e vuoi mantenerlo facilmente leggibile dall'uomo, puoi usare una versione semplificata della risposta di 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

1

Se hai bisogno solo di un dump rapido e non vuoi implementare un codificatore personalizzato. È possibile utilizzare quanto segue:

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

Questo convertirà tutti gli insiemi (e altri iterabili) in array. Fai attenzione che quei campi rimarranno array quando analizzi il json. Se si desidera preservare i tipi, è necessario scrivere un codificatore personalizzato.


7
Quando provo questo ottengo: TypeError: __init __ () ha ricevuto un argomento inaspettato per la parola chiave 'iterable_as_array'
atm

Devi installare simplejson
JerryBringer l'

importa simplejson come json e quindi json_string = json.dumps (data, iterable_as_array = True) funziona bene in Python 3.6
fraverta

1

Un difetto della soluzione accettata è che il suo output è molto specifico per Python. Cioè il suo output json non può essere osservato da un essere umano o caricato da un'altra lingua (ad esempio javascript). esempio:

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

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

Ti porterà:

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

Posso proporre una soluzione che riduce il set a un dict contenente un elenco all'uscita e di nuovo a un set quando caricato in Python usando lo stesso codificatore, preservando quindi l'osservabilità e l'agnosticismo del linguaggio:

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

Che ti dà:

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

Nota che serializzare un dizionario che ha un elemento con una chiave "__set__"romperà questo meccanismo. Quindi __set__ora è diventata una dictchiave riservata . Ovviamente sentiti libero di usare un'altra chiave, più profondamente offuscata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.