Wie JSON-Sets serialisieren?

148

Ich habe einen Python set, der Objekte mit __hash__und __eq__Methoden enthält , um sicherzustellen, dass keine Duplikate in der Sammlung enthalten sind.

Ich muss dieses Ergebnis json codieren set, aber das Übergeben eines Leerzeichens setan die json.dumpsMethode löst a aus 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

Ich weiß, dass ich eine Erweiterung für die json.JSONEncoderKlasse erstellen kann , die eine benutzerdefinierte defaultMethode hat, aber ich bin mir nicht einmal sicher, wo ich mit der Konvertierung über beginnen soll set. Sollte ich aus den setWerten der Standardmethode ein Wörterbuch erstellen und dann die Codierung dafür zurückgeben? Im Idealfall möchte ich die Standardmethode in die Lage versetzen, alle Datentypen zu verarbeiten, an denen der ursprüngliche Encoder erstickt (ich verwende Mongo als Datenquelle, sodass Daten diesen Fehler ebenfalls auslösen).

Jeder Hinweis in die richtige Richtung wäre willkommen.

BEARBEITEN:

Danke für die Antwort! Vielleicht hätte ich genauer sein sollen.

Ich habe die Antworten hier verwendet (und positiv bewertet), um die Einschränkungen der setÜbersetzung zu umgehen , aber es gibt auch interne Schlüssel, die ein Problem darstellen.

Die Objekte in setsind komplexe Objekte, die übersetzt werden __dict__, aber sie selbst können auch Werte für ihre Eigenschaften enthalten, die für die Basistypen im JSON-Encoder möglicherweise nicht geeignet sind.

Es gibt viele verschiedene Typen set, und der Hash berechnet im Grunde eine eindeutige ID für die Entität, aber im wahren Geist von NoSQL kann nicht genau gesagt werden, was das untergeordnete Objekt enthält.

Ein Objekt enthält möglicherweise einen Datumswert für starts, während ein anderes Objekt möglicherweise ein anderes Schema enthält, das keine Schlüssel enthält, die "nicht primitive" Objekte enthalten.

Deshalb ist die einzige Lösung , die ich denken konnte, war das zu erweitern , JSONEncoderdie ersetzen defaultMethode auf verschiedene Fälle zu drehen - aber ich bin nicht sicher , wie dies zu realisieren und die Dokumentation ist nicht eindeutig. Wird in verschachtelten Objekten der von defaultgo zurückgegebene Wert per Schlüssel angegeben, oder handelt es sich nur um ein generisches Einschließen / Verwerfen, das das gesamte Objekt betrachtet? Wie berücksichtigt diese Methode verschachtelte Werte? Ich habe frühere Fragen durchgesehen und kann anscheinend nicht den besten Ansatz für die fallspezifische Codierung finden (was leider so aussieht, wie ich es hier tun muss).

DeaconDesperado
quelle
3
warum dicts? Ich denke, Sie möchten nur ein listSet machen und es dann an den Encoder weitergeben ... zB:encode(list(myset))
Constantinius
2
Anstelle von JSON können Sie auch YAML verwenden (JSON ist im Wesentlichen eine Teilmenge von YAML).
Paolo Moretti
@PaoloMoretti: Bringt es aber einen Vorteil? Ich glaube nicht, dass Sets zu den allgemein unterstützten Datentypen von YAML gehören, und es wird weniger häufig unterstützt, insbesondere in Bezug auf APIs.
@PaoloMoretti Vielen Dank für Ihre Eingabe, aber das Anwendungs-Frontend erfordert JSON als Rückgabetyp und diese Anforderung ist für alle Zwecke festgelegt.
DeaconDesperado
2
@delnan Ich habe YAML vorgeschlagen, weil es eine native Unterstützung für Sets und Datumsangaben bietet .
Paolo Moretti

Antworten:

116

Die JSON- Notation enthält nur eine Handvoll nativer Datentypen (Objekte, Arrays, Zeichenfolgen, Zahlen, Boolesche Werte und Null). Daher muss alles, was in JSON serialisiert wird, als einer dieser Typen ausgedrückt werden.

Wie in den Dokumenten des JSON-Moduls gezeigt , kann diese Konvertierung automatisch von einem JSONEncoder und JSONDecoder durchgeführt werden. Dann würden Sie jedoch eine andere Struktur aufgeben, die Sie möglicherweise benötigen (wenn Sie Sets in eine Liste konvertieren, verlieren Sie die Fähigkeit, regelmäßig wiederherzustellen Listen; wenn Sie Sets mit in ein Wörterbuch konvertieren, dict.fromkeys(s)verlieren Sie die Fähigkeit, Wörterbücher wiederherzustellen.

Eine komplexere Lösung besteht darin, einen benutzerdefinierten Typ zu erstellen, der mit anderen nativen JSON-Typen koexistieren kann. Auf diese Weise können Sie verschachtelte Strukturen speichern, die Listen, Mengen, Diktate, Dezimalstellen, Datums- / Uhrzeitobjekte usw. enthalten.

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

Hier ist eine Beispielsitzung, die zeigt, dass sie Listen, Diktate und Mengen verarbeiten kann:

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

Alternativ kann es nützlich sein, eine allgemeinere Serialisierungstechnik wie YAML , Twisted Jelly oder Pythons Pickle-Modul zu verwenden . Diese unterstützen jeweils einen viel größeren Bereich von Datentypen.

Raymond Hettinger
quelle
11
Dies ist das erste Mal, dass ich gehört habe, dass YAML allgemeiner ist als JSON ... o_O
Karl Knechtel
13
@KarlKnechtel YAML ist eine Obermenge von JSON (fast). Außerdem werden Tags für Binärdaten, Sätze, geordnete Karten und Zeitstempel hinzugefügt. Mehr Datentypen zu unterstützen, ist das, was ich unter "allgemeiner Zweck" verstanden habe. Sie scheinen den Ausdruck "Allzweck" in einem anderen Sinne zu verwenden.
Raymond Hettinger
4
Vergessen Sie nicht auch jsonpickle , eine verallgemeinerte Bibliothek zum Einfügen von Python-Objekten in JSON, wie diese Antwort nahelegt.
Jason R. Coombs
4
Ab Version 1.2 ist YAML eine strikte Obermenge von JSON. Alle legalen JSON ist jetzt legal YAML. yaml.org/spec/1.2/spec.html
steveha
2
Dieses Codebeispiel importiert JSONDecoder, verwendet es aber nicht
Watsonic
115

Sie können einen benutzerdefinierten Encoder erstellen, der a zurückgibt, listwenn er auf a trifft set. Hier ist ein Beispiel:

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

Auf diese Weise können Sie auch andere Typen erkennen. Wenn Sie beibehalten möchten, dass die Liste tatsächlich ein Satz war, können Sie eine benutzerdefinierte Codierung verwenden. So etwas return {'type':'set', 'list':list(obj)}könnte funktionieren.

Um verschachtelte Typen zu veranschaulichen, sollten Sie Folgendes serialisieren:

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

Dies löst den folgenden Fehler aus:

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

Dies zeigt an, dass der Encoder das zurückgegebene listErgebnis übernimmt und den Serializer für seine untergeordneten Elemente rekursiv aufruft. So fügen Sie einen benutzerdefinierten Serializer für mehrere Typen hinzu:

>>> 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
quelle
Vielen Dank, ich habe die Frage bearbeitet, um besser zu spezifizieren, dass dies die Art von Dingen war, die ich brauchte. Was ich nicht verstehen kann, ist, wie diese Methode mit verschachtelten Objekten umgeht. In Ihrem Beispiel ist der Rückgabewert eine Liste für die Menge, aber was ist, wenn das übergebene Objekt eine Menge mit Datumsangaben (einem anderen fehlerhaften Datentyp) war? Sollte ich die Schlüssel innerhalb der Standardmethode selbst durchforsten? Danke vielmals!
DeaconDesperado
1
Ich denke, das JSON-Modul behandelt verschachtelte Objekte für Sie. Sobald die Liste wiederhergestellt ist, werden die Listenelemente durchlaufen, die versuchen, die einzelnen Elemente zu codieren. Wenn eines davon ein Datum ist, wird die defaultFunktion erneut aufgerufen, diesmal objals Datumsobjekt. Sie müssen es nur testen und eine Datumsdarstellung zurückgeben.
Jterrace
Die Standardmethode könnte also möglicherweise mehrmals für ein an sie übergebenes Objekt ausgeführt werden, da sie auch die einzelnen Schlüssel überprüft, sobald sie "aufgelistet" ist.
DeaconDesperado
Es wird nicht mehrmals für dasselbe Objekt aufgerufen , kann aber in untergeordnete Objekte umgewandelt werden. Siehe aktualisierte Antwort.
Jterrace
Hat genau so funktioniert, wie Sie es beschrieben haben. Ich muss noch einige der Fehler herausfinden, aber das meiste davon ist wahrscheinlich etwas, das überarbeitet werden kann. Vielen Dank für Ihre Anleitung!
DeaconDesperado
7

Ich habe Raymond Hettingers Lösung an Python 3 angepasst .

Folgendes hat sich geändert:

  • unicode verschwunden
  • aktualisierte den Anruf an die Eltern defaultmitsuper()
  • Verwenden Sie, base64um den bytesTyp in zu serialisieren str(da es scheint, dass bytesin Python 3 nicht in JSON konvertiert werden kann)
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
quelle
4
Der Code am Ende gezeigt diese Antwort auf eine ähnliche Frage erreicht , indem die gleichen [nur] Decodierung und Encodierung der Bytes Objekt json.dumps()kehrt in / aus 'latin1', das Überspringen von base64Sachen , die nicht notwendig ist.
Martineau
6

In JSON sind nur Wörterbücher, Listen und primitive Objekttypen (int, string, bool) verfügbar.

Joseph Le Brech
quelle
5
"Primitiver Objekttyp" macht keinen Sinn, wenn es um Python geht. "Eingebautes Objekt" ist sinnvoller, aber hier zu weit gefasst (für den Anfang: Es enthält Diktate, Listen und auch Mengen). (JSON-Terminologie kann jedoch unterschiedlich sein.)
Zeichenfolge Nummer Objekt Array wahr falsch null
Joseph Le Brech
6

Sie müssen keine benutzerdefinierte Encoder-Klasse defaulterstellen , um die Methode bereitzustellen. Sie kann als Schlüsselwortargument übergeben werden:

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)

führt zu [1, 2, 3]allen unterstützten Python-Versionen.

Antti Haapala
quelle
4

Wenn Sie nur Mengen und keine allgemeinen Python-Objekte codieren müssen und diese leicht lesbar halten möchten, können Sie eine vereinfachte Version der Antwort von Raymond Hettinger verwenden:

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

Wenn Sie nur einen schnellen Speicherauszug benötigen und keinen benutzerdefinierten Encoder implementieren möchten. Sie können Folgendes verwenden:

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

Dadurch werden alle Mengen (und andere iterable) in Arrays konvertiert. Achten Sie nur darauf, dass diese Felder Arrays bleiben, wenn Sie den JSON zurück analysieren. Wenn Sie die Typen beibehalten möchten, müssen Sie einen benutzerdefinierten Encoder schreiben.

David Novák
quelle
7
Wenn ich das versuche, erhalte ich: TypeError: __init __ () hat ein unerwartetes Schlüsselwortargument 'iterable_as_array'
atm
Sie müssen simplejson
JerryBringer
importiere simplejson als json und dann json_string = json.dumps (data, iterable_as_array = True) funktioniert gut in Python 3.6
fraverta
1

Ein Nachteil der akzeptierten Lösung besteht darin, dass ihre Ausgabe sehr pythonspezifisch ist. Das heißt, seine rohe JSON-Ausgabe kann von einem Menschen nicht beobachtet oder von einer anderen Sprache (z. B. Javascript) geladen werden. Beispiel:

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

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

Kriege dich:

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

Ich kann eine Lösung vorschlagen, die das Set auf ein Diktat herabstuft, das auf dem Weg nach draußen eine Liste enthält, und zurück zu einem Set, wenn es mit demselben Encoder in Python geladen wird, wodurch Beobachtbarkeit und Sprachunabhängigkeit erhalten bleiben:

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

Was bringt dich:

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

Beachten Sie, dass das Serialisieren eines Wörterbuchs, das ein Element mit einem Schlüssel enthält "__set__", diesen Mechanismus unterbricht. So __set__ist jetzt ein reservierter dictSchlüssel geworden. Sie können natürlich auch einen anderen, tiefer verschleierten Schlüssel verwenden.

Sagismus
quelle