Jak serializować zestawy JSON?

149

Mam Python set, który zawiera obiekty z __hash__i __eq__metod w celu dokonania pewnych duplikatów nie są zawarte w zbiorze.

Muszę zakodować ten wynik w setformacie json , ale przekazanie nawet pustego elementu setdo json.dumpsmetody wywołuje 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

Wiem, że mogę utworzyć rozszerzenie json.JSONEncoderklasy, która ma niestandardową defaultmetodę, ale nie jestem nawet pewien, od czego zacząć konwertowanie na set. Czy powinienem utworzyć słownik z setwartości w metodzie domyślnej, a następnie zwrócić kodowanie? Idealnie chciałbym, aby domyślna metoda była w stanie obsłużyć wszystkie typy danych, na których dusi oryginalny koder (używam Mongo jako źródła danych, więc daty wydają się również powodować ten błąd)

Każda wskazówka we właściwym kierunku będzie mile widziana.

EDYTOWAĆ:

Dziękuję za odpowiedź! Może powinienem był być bardziej precyzyjny.

Wykorzystałem (i przegłosowałem) odpowiedzi tutaj, aby ominąć ograniczenia settłumaczenia, ale istnieją również klucze wewnętrzne, które również stanowią problem.

Obiekty w pliku setsą złożonymi obiektami, które są tłumaczone na __dict__, ale same mogą również zawierać wartości swoich właściwości, które mogą nie kwalifikować się do podstawowych typów w koderze json.

Wchodzi w to wiele różnych typów set, a hash w zasadzie oblicza unikalny identyfikator jednostki, ale w prawdziwym duchu NoSQL nie można dokładnie określić, co zawiera obiekt potomny.

Jeden obiekt może zawierać wartość daty dla starts, podczas gdy inny może mieć inny schemat, który nie zawiera kluczy zawierających obiekty „nieprymitywne”.

Dlatego jedynym rozwiązaniem, jakie przyszło mi do głowy, było rozszerzenie, JSONEncoderaby zastąpić defaultmetodę włączania różnych przypadków - ale nie jestem pewien, jak się do tego zabrać, a dokumentacja jest niejednoznaczna. Czy w obiektach zagnieżdżonych wartość zwracana defaultprzechodzi przez klucz, czy jest to tylko ogólne dołączenie / odrzucenie, które patrzy na cały obiekt? W jaki sposób ta metoda obsługuje zagnieżdżone wartości? Przejrzałem poprzednie pytania i nie mogę znaleźć najlepszego podejścia do kodowania specyficznego dla przypadku (co niestety wydaje się być tym, co będę musiał tutaj zrobić).

DeaconDesperado
źródło
3
dlaczego dict? Myślę, że chcesz po prostu listwyjść z planu, a następnie przekazać to do enkodera ... np:encode(list(myset))
Constantinius
2
Zamiast używać JSON, możesz użyć YAML (JSON jest zasadniczo podzbiorem YAML).
Paolo Moretti
@PaoloMoretti: Czy to jednak przynosi korzyści? Nie sądzę, by zestawy były jednymi z powszechnie obsługiwanych typów danych YAML i jest mniej szeroko obsługiwany, szczególnie w odniesieniu do interfejsów API.
@PaoloMoretti Dziękuję za dane wejściowe, ale nakładka aplikacji wymaga JSON jako typu zwracanego i ten wymóg jest do wszystkich celów naprawiony.
DeaconDesperado
2
@delnan Sugerowałem YAML, ponieważ ma natywne wsparcie zarówno dla zestawów, jak i dat .
Paolo Moretti

Odpowiedzi:

116

Notacja JSON ma tylko kilka rodzimych typów danych (obiekty, tablice, ciągi, liczby, wartości logiczne i null), więc wszystko, co jest serializowane w JSON, musi być wyrażone jako jeden z tych typów.

Jak pokazano w dokumentacji modułu json , ta konwersja może być wykonana automatycznie przez JSONEncoder i JSONDecoder , ale wtedy zrezygnujesz z innej struktury, której możesz potrzebować (jeśli przekonwertujesz zestawy na listę, stracisz możliwość regularnego odzyskiwania listy; jeśli przekonwertujesz zestawy na słownik za pomocą dict.fromkeys(s), utracisz możliwość odzyskiwania słowników).

Bardziej wyrafinowanym rozwiązaniem jest utworzenie niestandardowego typu, który może współistnieć z innymi natywnymi typami JSON. Pozwala to na przechowywanie zagnieżdżonych struktur, które obejmują listy, zbiory, dykty, liczby dziesiętne, obiekty daty i godziny itp .:

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

Oto przykładowa sesja pokazująca, że ​​może obsługiwać listy, dykty i zestawy:

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

Alternatywnie przydatne może być użycie techniki serializacji bardziej ogólnego przeznaczenia, takiej jak YAML , Twisted Jelly lub Python's pickle module . Każdy z nich obsługuje znacznie większy zakres typów danych.

Raymond Hettinger
źródło
11
To jest pierwsza, o której słyszałem, że YAML jest bardziej uniwersalny niż JSON ... o_O
Karl Knechtel
13
@KarlKnechtel YAML to nadzbiór JSON (prawie). Dodaje również znaczniki do danych binarnych, zbiorów, uporządkowanych map i znaczników czasu. Obsługa większej liczby typów danych jest tym, co rozumiałem przez „bardziej ogólny cel”. Wydaje się, że używasz wyrażenia „ogólny cel” w innym sensie.
Raymond Hettinger
4
Nie zapomnij również o jsonpickle , który ma być uogólnioną biblioteką do wytrawiania obiektów Pythona do JSON, tak jak sugeruje ta odpowiedź.
Jason R. Coombs
4
Od wersji 1.2 YAML jest ścisłym nadzbiorem JSON. Wszystkie legalne JSON są teraz legalne YAML. yaml.org/spec/1.2/spec.html
steveha
2
ten przykład kodu importuje, JSONDecoderale go nie używa
watsonic
115

Możesz utworzyć koder niestandardowy, który zwraca kod, listgdy napotka plik set. Oto przykład:

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

W ten sposób możesz również wykryć inne typy. Jeśli chcesz zachować, że lista była w rzeczywistości zestawem, możesz użyć niestandardowego kodowania. Coś takiego return {'type':'set', 'list':list(obj)}może zadziałać.

Aby zilustrować zagnieżdżone typy, rozważ serializację tego:

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

Powoduje to następujący błąd:

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

Oznacza to, że koder pobierze listzwrócony wynik i rekurencyjnie wywoła serializator w swoich elementach podrzędnych. Aby dodać niestandardowy serializator dla wielu typów, możesz to zrobić:

>>> 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
źródło
Dzięki, zredagowałem pytanie, aby lepiej określić, że tego potrzebowałem. Wydaje się, że nie mogę pojąć, jak ta metoda będzie obsługiwać obiekty zagnieżdżone. W naszym przykładzie zwracana wartość to list for set, ale co by się stało, gdyby przekazany obiekt był zestawem z datami (inny zły typ danych) w środku? Czy powinienem drążyć klucze w samej metodzie domyślnej? Wielkie dzięki!
DeaconDesperado
1
Myślę, że moduł JSON obsługuje dla Ciebie obiekty zagnieżdżone. Gdy otrzyma listę z powrotem, będzie iterować po elementach listy, próbując zakodować każdy z nich. Jeśli jedna z nich jest datą, defaultfunkcja zostanie wywołana ponownie, tym razem objjako obiekt daty, więc wystarczy ją przetestować i zwrócić reprezentację daty.
jterrace
Czyli domyślna metoda mogłaby być wykonywana kilka razy dla dowolnego przekazanego do niej obiektu, ponieważ będzie ona również sprawdzać poszczególne klucze, gdy zostanie „wylistowana”?
DeaconDesperado
W pewnym sensie nie zostanie wywołany wiele razy dla tego samego obiektu, ale może powtórzyć się w potomkach. Zobacz zaktualizowaną odpowiedź.
jterrace
Działało dokładnie tak, jak opisałeś. Nadal muszę rozwiązać niektóre usterki, ale większość z nich to prawdopodobnie rzeczy, które można naprawić. Wielkie dzięki za wskazówki!
DeaconDesperado
7

Dostosowałem rozwiązanie Raymonda Hettingera do Pythona 3.

Oto, co się zmieniło:

  • unicode zniknął
  • zaktualizował wezwanie do rodziców defaultzsuper()
  • za pomocą base64serializacji bytestypu do str(ponieważ wydaje się, że bytesw Pythonie 3 nie można przekonwertować na 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
źródło
4
Kod pokazany na końcu tej odpowiedzi na pokrewne pytanie osiąga to samo przez [tylko] dekodowanie i zakodowanie obiektu bajtów, który json.dumps()powraca do / z 'latin1', pomijając base64rzeczy, które nie są konieczne.
martineau
6

W formacie JSON dostępne są tylko słowniki, listy i prymitywne typy obiektów (int, string, bool).

Joseph Le Brech
źródło
5
„Prymitywny typ obiektu” nie ma sensu, gdy mówimy o Pythonie. „Obiekt wbudowany” ma więcej sensu, ale jest tutaj zbyt szeroki (na początek: obejmuje dykty, listy, a także zbiory). (Terminologia JSON może być jednak inna.)
string number object tablica prawda fałsz null
Joseph Le Brech
6

Nie musisz tworzyć niestandardowej klasy kodera, aby dostarczać defaultmetodę - można ją przekazać jako argument słowa kluczowego:

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)

wyniki we [1, 2, 3]wszystkich obsługiwanych wersjach języka Python.

Antti Haapala
źródło
4

Jeśli potrzebujesz tylko zakodować zestawy, a nie ogólne obiekty Pythona i chcesz, aby były one łatwe do odczytania przez człowieka, możesz użyć uproszczonej wersji odpowiedzi Raymonda Hettingera:

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
źródło
1

Jeśli potrzebujesz tylko szybkiego zrzutu i nie chcesz wdrażać niestandardowego kodera. Możesz użyć następujących:

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

Spowoduje to przekonwertowanie wszystkich zestawów (i innych elementów iteracyjnych) na tablice. Uważaj tylko, że te pola pozostaną tablicami, gdy ponownie przeanalizujesz json. Jeśli chcesz zachować typy, musisz napisać niestandardowy koder.

David Novák
źródło
7
Kiedy próbuję tego, otrzymuję: TypeError: __init __ () otrzymałem nieoczekiwany argument słowa kluczowego „iterable_as_array”
atm
Musisz zainstalować simplejson
JerryBringer
import simpleejson as json, a następnie json_string = json.dumps (data, iterable_as_array = True) działa dobrze w Pythonie 3.6
fraverta
1

Jedną z wad przyjętego rozwiązania jest to, że jego dane wyjściowe są bardzo specyficzne dla Pythona. Oznacza to, że jego surowe wyjście json nie może być obserwowane przez człowieka ani załadowane przez inny język (np. Javascript). przykład:

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

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

Dostaniesz:

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

Mogę zaproponować rozwiązanie, które obniża ocenę zestawu do dyktowania zawierającego listę po wyjściu i z powrotem do zestawu po załadowaniu do Pythona za pomocą tego samego kodera, zachowując w ten sposób obserwowalność i agnostycyzm językowy:

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

Co daje ci:

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

Uwaga że serializacja słownika, który ma element z kluczem "__set__", zepsuje ten mechanizm. Więc __set__teraz stał się zarezerwowanym dictkluczem. Oczywiście możesz użyć innego, głębiej zaciemnionego klucza.

sagizm
źródło