Serializacja instancji klasy do JSON

186

Próbuję utworzyć reprezentację ciągu JSON wystąpienia klasy i mam trudności. Powiedzmy, że klasa jest zbudowana w następujący sposób:

class testclass:
    value1 = "a"
    value2 = "b"

Wywołanie json.dumps odbywa się w następujący sposób:

t = testclass()
json.dumps(t)

Nie udaje się i mówi mi, że klasa testowa nie jest serializowalna w JSON.

TypeError: <__main__.testclass object at 0x000000000227A400> is not JSON serializable

Próbowałem także użyć modułu marynowanego:

t = testclass()
print(pickle.dumps(t, pickle.HIGHEST_PROTOCOL))

I daje informacje o instancji klasy, ale nie szereguje treści instancji klasy.

b'\x80\x03c__main__\ntestclass\nq\x00)\x81q\x01}q\x02b.'

Co ja robię źle?

ferhan
źródło
30
Użyj jednej linii s = json.dumps(obj, default=lambda x: x.__dict__), do zmiennych instancji serializacji obiektu ( self.value1, self.value2...). To najprostszy i najprostszy sposób. Będzie serializować struktury obiektów zagnieżdżonych. defaultFunkcja jest wywoływana, gdy dany obiekt nie jest bezpośrednio serializacji. Możesz również spojrzeć na moją odpowiedź poniżej. Znalazłem popularne odpowiedzi niepotrzebnie złożone, które prawdopodobnie były prawdziwe już dawno temu.
codeman48
1
Twoja testclassnie ma __init__()metody, więc wszystkie instancje będą miały te same atrybuty klasy ( value1i value2) zdefiniowane w instrukcji class. Czy rozumiesz różnicę między klasą a instancją jednej?
martineau
1
Istnieje biblioteka python dla tego github.com/jsonpickle/jsonpickle (komentowanie, ponieważ odpowiedź jest zbyt niska w wątku i nie będzie dostępna).
najlepsze życzenia

Odpowiedzi:

237

Podstawowym problemem jest to, że koder JSON json.dumps()domyślnie serializuje tylko ograniczony zestaw typów obiektów, wszystkie typy wbudowane. Wymień tutaj: https://docs.python.org/3.3/library/json.html#encoders-and-decoders

Jednym dobrym rozwiązaniem byłoby, aby Twoja klasa odziedziczyła, JSONEncodera następnie zaimplementowaćJSONEncoder.default() funkcję i sprawiła, że ​​ta funkcja emituje poprawny JSON dla twojej klasy.

Rozwiązanie proste byłoby zadzwonić json.dumps()na .__dict__członka tej instancji. Jest to standardowy Python dicti jeśli twoja klasa jest prosta, będzie można serializować JSON.

class Foo(object):
    def __init__(self):
        self.x = 1
        self.y = 2

foo = Foo()
s = json.dumps(foo) # raises TypeError with "is not JSON serializable"

s = json.dumps(foo.__dict__) # s set to: {"x":1, "y":2}

Powyższe podejście omówiono w tym blogu:

    Serializacja dowolnych obiektów Python do JSON przy użyciu __dict__

steveha
źródło
3
Próbowałem tego. Końcowym wynikiem wywołania json.dumps (t .__ dict__) jest po prostu {}.
ferhan
6
Jest tak, ponieważ twoja klasa nie ma .__init__()funkcji metody, więc instancja klasy ma pusty słownik. Innymi słowy, {}jest poprawnym wynikiem dla twojego przykładowego kodu.
steveha,
3
Dzięki. To załatwia sprawę. Dodałem prosty init bez parametrów i teraz wywołanie json.dumps (t .__ dict__) zwraca prawidłowe dane w formacie: {„value2”: „345”, „value1”: „123”} Widziałem posty takie jak wcześniej nie byłem pewien, czy potrzebuję niestandardowego serializatora dla członków, potrzeba inicjacji nie została wyraźnie wymieniona, czy też mi tego brakowało. Dziękuję Ci.
ferhan
3
Praca dla jednej klasy, ale nie z pokrewnymi klasami
Nwawel A Iroume
2
@NwawelAIroume: True. Jeśli przedmiot, który jest np zawierający wiele obiektów na liście błąd jest nadalis not JSON serializable
gies0r
57

Jest dla mnie świetny sposób, który możesz wypróbować:

json.dumps()może przyjąć opcjonalny parametr domyślny, w którym można określić niestandardową funkcję serializatora dla nieznanych typów, która w moim przypadku wygląda

def serialize(obj):
    """JSON serializer for objects not serializable by default json code"""

    if isinstance(obj, date):
        serial = obj.isoformat()
        return serial

    if isinstance(obj, time):
        serial = obj.isoformat()
        return serial

    return obj.__dict__

Pierwsze dwa ifs dotyczą serializacji daty i godziny, a potem jest obj.__dict__ zwracany jest dowolny inny obiekt.

ostatnie połączenie wygląda następująco:

json.dumps(myObj, default=serialize)

Jest to szczególnie dobre, gdy serializujesz kolekcję i nie chcesz dzwonić __dict__ jawnie dla każdego obiektu. Tutaj zrobisz to automatycznie.

Do tej pory działało tak dobrze dla mnie, czekając na twoje przemyślenia.

brokuły
źródło
I dostać NameError: name 'serialize' is not defined. Jakieś wskazówki?
Kyle Delaney
Bardzo dobrze. Tylko dla klas, które mają automaty:try: dict = obj.__dict__ except AttributeError: dict = {s: getattr(obj, s) for s in obj.__slots__ if hasattr(obj, s)} return dict
fantastory
To niesamowite, że w tak popularnym języku nie ma jednej wykładziny, która pozwoliłaby zachować obiekt. Musi być, ponieważ nie jest wpisany statycznie.
TheRennen
49

Możesz podać defaultnazwany parametr w json.dumps()funkcji:

json.dumps(obj, default=lambda x: x.__dict__)

Wyjaśnienie:

Utwórz dokumenty ( 2.7 , 3.6 ):

``default(obj)`` is a function that should return a serializable version
of obj or raise TypeError. The default simply raises TypeError.

(Działa na Python 2.7 i Python 3.x)

Uwaga: W tym przypadku potrzebujesz instancezmiennych, a nie classzmiennych, tak jak w przykładzie z pytania. (Zakładam, że pytający miał class instancebyć przedmiotem klasy)

Nauczyłem się tego po raz pierwszy z odpowiedzi @ phihag tutaj . Okazało się, że jest to najprostszy i najczystszy sposób na wykonanie zadania.

codeman48
źródło
6
To działało dla mnie, ale z powodu członków datetime.date zmieniłem to nieco:default=lambda x: getattr(x, '__dict__', str(x))
Dakota Hawkins,
@Dakota ładne obejście; datetime.datejest implementacją C, dlatego nie ma __dict__atrybutu. IMHO ze względu na ujednolicenie, datetime.datepowinno go mieć ...
codeman48,
22

Po prostu robię:

data=json.dumps(myobject.__dict__)

To nie jest pełna odpowiedź, a jeśli masz jakąś skomplikowaną klasę obiektów, na pewno nie dostaniesz wszystkiego. Jednak używam tego do niektórych moich prostych obiektów.

Jedną z nich, na której działa naprawdę dobrze, jest klasa „options” uzyskana z modułu OptionParser. Tutaj jest wraz z samym żądaniem JSON.

  def executeJson(self, url, options):
        data=json.dumps(options.__dict__)
        if options.verbose:
            print data
        headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
        return requests.post(url, data, headers=headers)
SpiRail
źródło
Możesz usunąć siebie, jeśli nie używasz tego w klasie.
SpiRail,
3
Działa to dobrze, o ile obiekt nie jest złożony z innych obiektów.
Haroldo_OK
19

Korzystanie z jsonpickle

import jsonpickle

object = YourClass()
json_object = jsonpickle.encode(object)
gies0r
źródło
5

JSON nie jest tak naprawdę przeznaczony do serializacji dowolnych obiektów Pythona. Świetnie nadaje się do szeregowania dictobiektów, ale picklemoduł jest naprawdę tym, czego powinieneś ogólnie używać. Dane wyjściowe picklenie są tak naprawdę czytelne dla człowieka, ale powinny być w porządku. Jeśli nalegasz na użycie JSON, możesz sprawdzićjsonpickle moduł, który jest interesującym podejściem hybrydowym.

https://github.com/jsonpickle/jsonpickle

Brendan Wood
źródło
9
Główny problem, jaki widzę w przypadku marynaty, polega na tym, że jest to format specyficzny dla Pythona, podczas gdy JSON jest formatem niezależnym od platformy. JSON jest szczególnie przydatny, jeśli piszesz aplikację internetową lub backend dla niektórych aplikacji mobilnych. To powiedziawszy, dziękuję za zwrócenie uwagi na Jsonpickle.
Haroldo_OK
@Haroldo_OK Czy jsonpickle nadal eksportuje do JSON, po prostu nie jest bardzo czytelny dla człowieka?
Caelum
4

Oto dwie proste funkcje serializacji dowolnych niewyrafinowanych klas, nic szczególnego, jak wyjaśniono wcześniej.

Używam tego do konfiguracji typu, ponieważ mogę dodawać nowych członków do klas bez żadnych modyfikacji kodu.

import json

class SimpleClass:
    def __init__(self, a=None, b=None, c=None):
        self.a = a
        self.b = b
        self.c = c

def serialize_json(instance=None, path=None):
    dt = {}
    dt.update(vars(instance))

    with open(path, "w") as file:
        json.dump(dt, file)

def deserialize_json(cls=None, path=None):
    def read_json(_path):
        with open(_path, "r") as file:
            return json.load(file)

    data = read_json(path)

    instance = object.__new__(cls)

    for key, value in data.items():
        setattr(instance, key, value)

    return instance

# Usage: Create class and serialize under Windows file system.
write_settings = SimpleClass(a=1, b=2, c=3)
serialize_json(write_settings, r"c:\temp\test.json")

# Read back and rehydrate.
read_settings = deserialize_json(SimpleClass, r"c:\temp\test.json")

# results are the same.
print(vars(write_settings))
print(vars(read_settings))

# output:
# {'c': 3, 'b': 2, 'a': 1}
# {'c': 3, 'b': 2, 'a': 1}
GBGOLC
źródło
3

Istnieje kilka dobrych odpowiedzi na temat tego, jak zacząć to robić. Ale należy pamiętać o kilku rzeczach:

  • Co jeśli instancja jest zagnieżdżona w dużej strukturze danych?
  • Co jeśli też chcesz nazwę klasy?
  • Co zrobić, jeśli chcesz zdezrializować instancję?
  • Co jeśli używasz __slots__zamiast __dict__?
  • Co jeśli po prostu nie chcesz tego zrobić?

json-tricks to biblioteka (którą stworzyłem i do której przyczyniłem się inni), która była w stanie to zrobić od dłuższego czasu. Na przykład:

class MyTestCls:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)

cls_instance = MyTestCls(s='ub', dct={'7': 7})

json = dumps(cls_instance, indent=4)
instance = loads(json)

Odzyskasz instancję. Tutaj Json wygląda tak:

{
    "__instance_type__": [
        "json_tricks.test_class",
        "MyTestCls"
    ],
    "attributes": {
        "s": "ub",
        "dct": {
            "7": 7
        }
    }
}

Jeśli lubisz tworzyć własne rozwiązania, możesz spojrzeć na źródło, json-tricksaby nie zapomnieć o specjalnych przypadkach (jak __slots__).

Robi także inne typy, takie jak tablice numpy, czasy danych, liczby zespolone; pozwala również na komentarze.

znak
źródło
3

Python3.x

Najlepsze podejście, jakie mogłem osiągnąć dzięki mojej wiedzy, było takie.
Zauważ, że ten kod również traktuje set ().
To podejście jest ogólne, wymaga jedynie rozszerzenia klasy (w drugim przykładzie).
Zauważ, że robię to tylko dla plików, ale łatwo jest zmodyfikować zachowanie według własnego uznania.

Jest to jednak CoDec.

Przy odrobinie pracy możesz zbudować swoją klasę na inne sposoby. Zakładam, że domyślny konstruktor to instancja, a następnie aktualizuję dyktę klasową.

import json
import collections


class JsonClassSerializable(json.JSONEncoder):

    REGISTERED_CLASS = {}

    def register(ctype):
        JsonClassSerializable.REGISTERED_CLASS[ctype.__name__] = ctype

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in self.REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = self.REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


JsonClassSerializable.register(C)


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


JsonClassSerializable.register(B)


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()

JsonClassSerializable.register(A)

A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
print(b.b)
print(b.c.a)

Edytować

Po dalszych badaniach znalazłem sposób na uogólnienie bez konieczności wywoływania metody rejestrowania SUPERCLASS za pomocą metaklasy

import json
import collections

REGISTERED_CLASS = {}

class MetaSerializable(type):

    def __call__(cls, *args, **kwargs):
        if cls.__name__ not in REGISTERED_CLASS:
            REGISTERED_CLASS[cls.__name__] = cls
        return super(MetaSerializable, cls).__call__(*args, **kwargs)


class JsonClassSerializable(json.JSONEncoder, metaclass=MetaSerializable):

    def default(self, obj):
        if isinstance(obj, collections.Set):
            return dict(_set_object=list(obj))
        if isinstance(obj, JsonClassSerializable):
            jclass = {}
            jclass["name"] = type(obj).__name__
            jclass["dict"] = obj.__dict__
            return dict(_class_object=jclass)
        else:
            return json.JSONEncoder.default(self, obj)

    def json_to_class(self, dct):
        if '_set_object' in dct:
            return set(dct['_set_object'])
        elif '_class_object' in dct:
            cclass = dct['_class_object']
            cclass_name = cclass["name"]
            if cclass_name not in REGISTERED_CLASS:
                raise RuntimeError(
                    "Class {} not registered in JSON Parser"
                    .format(cclass["name"])
                )
            instance = REGISTERED_CLASS[cclass_name]()
            instance.__dict__ = cclass["dict"]
            return instance
        return dct

    def encode_(self, file):
        with open(file, 'w') as outfile:
            json.dump(
                self.__dict__, outfile,
                cls=JsonClassSerializable,
                indent=4,
                sort_keys=True
            )

    def decode_(self, file):
        try:
            with open(file, 'r') as infile:
                self.__dict__ = json.load(
                    infile,
                    object_hook=self.json_to_class
                )
        except FileNotFoundError:
            print("Persistence load failed "
                  "'{}' do not exists".format(file)
                  )


class C(JsonClassSerializable):

    def __init__(self):
        self.mill = "s"


class B(JsonClassSerializable):

    def __init__(self):
        self.a = 1230
        self.c = C()


class A(JsonClassSerializable):

    def __init__(self):
        self.a = 1
        self.b = {1, 2}
        self.c = B()


A().encode_("test")
b = A()
b.decode_("test")
print(b.a)
# 1
print(b.b)
# {1, 2}
print(b.c.a)
# 1230
print(b.c.c.mill)
# s
Davi Abreu Wasserberg
źródło
2

Uważam, że zamiast dziedziczenia, jak sugerowano w zaakceptowanej odpowiedzi, lepiej jest użyć polimorfizmu. W przeciwnym razie musisz mieć dużą instrukcję if if, aby dostosować kodowanie każdego obiektu. Oznacza to, że utwórz ogólny domyślny koder dla JSON jako:

def jsonDefEncoder(obj):
   if hasattr(obj, 'jsonEnc'):
      return obj.jsonEnc()
   else: #some default behavior
      return obj.__dict__

a następnie mieć jsonEnc()funkcję w każdej klasie, którą chcesz serializować. na przykład

class A(object):
   def __init__(self,lengthInFeet):
      self.lengthInFeet=lengthInFeet
   def jsonEnc(self):
      return {'lengthInMeters': lengthInFeet * 0.3 } # each foot is 0.3 meter

Potem dzwonisz json.dumps(classInstance,default=jsonDefEncoder)

Hwat
źródło