Серіалізація екземпляра класу до JSON


186

Я намагаюся створити JSON-рядкове представлення екземпляра класу і у мене виникли труднощі. Скажімо, клас побудований так:

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

Виклик на json.dumps робиться так:

t = testclass()
json.dumps(t)

Він провалюється і каже мені, що тестовий клас не є JSON-серіалізаційним.

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

Я також спробував використовувати модуль соління:

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

Він дає інформацію про екземпляр класу, але не серіалізований вміст екземпляра класу.

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

Що я роблю неправильно?



30
Використовуйте один рядок, s = json.dumps(obj, default=lambda x: x.__dict__)до змінних екземпляра сериализации об'єкта ( self.value1, self.value2, ...). Це найпростіший і найбільш прямий шлях вперед. Це буде серіалізувати вкладені об'єктні структури. defaultФункція викликається , коли який - або даний об'єкт безпосередньо не сериализации. Ви також можете подивитися на мою відповідь нижче. Я знайшов популярні відповіді надмірно складними, які, ймовірно, були досить давніми.
codeman48

1
У testclassвас немає __init__()методу, тому всі екземпляри матимуть однакові два атрибути класу ( value1та value2), визначені в операторі класу. Чи розумієте ви різницю між класом та екземпляром одного?
мартіно

1
Існує бібліотека python для цього github.com/jsonpickle/jsonpickle (коментуючи, оскільки відповідь занадто внизу в темі і не буде доступною.)
найкращі побажання

Відповіді:


237

Основна проблема полягає в тому, що кодер JSON json.dumps()знає, як серіалізувати обмежений набір типів об'єктів за замовчуванням, всі вбудовані типи. Список тут: https://docs.python.org/3.3/library/json.html#encoders-and-decoders

Хорошим рішенням буде зробити так, щоб ваш клас успадкував, JSONEncoderа потім реалізував цю JSONEncoder.default()функцію і змусив цю функцію видавати правильний JSON для вашого класу.

Просте рішення було б назвати json.dumps()на .__dict__члені цього примірника. Це стандартний Python, dictі якщо ваш клас простий, це буде 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}

Вищеописаний підхід обговорюється в цій публікації в блозі:

    Серіалізація довільних об’єктів Python до JSON за допомогою __dict__


3
Я спробував це. Кінцевий результат дзвінка на json.dumps (t .__ dict__) якраз {}.
ferhan

6
Це тому, що ваш клас не має .__init__()функції методу, тому у вашому екземплярі класу є порожній словник. Іншими словами, {}це правильний результат для вашого прикладу коду.
steveha

3
Дякую. Це робить трюк. Я додав простий init без параметрів, і тепер виклик json.dumps (t .__ dict__) повертає належні дані у форматі: {"value2": "345", "value1": "123"} Я бачив повідомлення, як це раніше, не був впевнений, чи потрібен мені користувальницький серіалізатор для членів, про необхідність init не згадувалось прямо або я пропустив його. Дякую.
ferhan

3
Ця робота для одного класу, але не з пов’язаними
об’єктами

2
@NwawelAIroume: Правда. Якщо у вас є об’єкт, який, наприклад, містить декілька об'єктів у списку, помилка все ще залишаєтьсяis not JSON serializable
gies0r

57

Є один із способів, який для мене чудово справляється:

json.dumps()може прийняти необов'язковий параметр за замовчуванням, де ви можете вказати спеціальну функцію серіалізатора для невідомих типів, що в моєму випадку виглядає так

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__

Перші два ifs - це серіялізація дати та часу, а потім є obj.__dict__повернення для будь-якого іншого об'єкта.

остаточний дзвінок виглядає так:

json.dumps(myObj, default=serialize)

Це особливо добре, коли ви серіалізуєте колекцію і не хочете __dict__явно телефонувати по кожному об'єкту. Тут це робиться для вас автоматично.

Поки працював так добре для мене, з нетерпінням чекаю ваших думок.


Я отримую NameError: name 'serialize' is not defined. Якісь поради?
Кайл Делані

Дуже хороша. Тільки для класів, які мають слоти:try: dict = obj.__dict__ except AttributeError: dict = {s: getattr(obj, s) for s in obj.__slots__ if hasattr(obj, s)} return dict
фантастика

Дивно, що на такій популярній мові немає жодного вкладиша для jsoniny об'єкта. Повинно бути, оскільки це не статично набрано.
TheRennen

49

Ви можете вказати defaultназваний параметр у json.dumps()функції:

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

Пояснення:

Сформуйте документи ( 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.

(Працює на Python 2.7 та Python 3.x)

Примітка. У цьому випадку вам потрібні instanceзмінні, а не classзмінні, як намагається зробити приклад у запитанні. (Я припускаю, що аскер мав class instanceна увазі об'єкт класу)

Я дізнався це спочатку з відповіді @ phihag тут . Виявив, що це найпростіший і найчистіший спосіб виконати роботу.


6
Це працювало для мене, але через членів datetime.date я його трохи змінив:default=lambda x: getattr(x, '__dict__', str(x))
Дакота Хокінс

@Dakota приємний обхід; datetime.dateце реалізація C, отже, вона не має __dict__атрибутів. ІМХО заради рівномірності, datetime.dateмав би його мати ...
codeman48

22

Я просто роблю:

data=json.dumps(myobject.__dict__)

Це не повна відповідь, і якщо у вас є якийсь складний клас об’єктів, ви, звичайно, не отримаєте все. Однак я використовую це для деяких моїх простих об'єктів.

Один, на який він працює дуже добре, - це клас "параметри", який ви отримуєте від модуля OptionParser. Ось це разом із самим запитом 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

3
Це буде добре, доки об’єкт не буде складений з інших об'єктів.
Haroldo_OK


5

JSON насправді не призначений для серіалізації довільних об’єктів Python. Це чудово для серіалізації dictоб’єктів, але pickleмодуль - це справді те, що вам слід використовувати. Вихід з pickleнасправді не є читабельним для людини, але його слід просто відібрати. Якщо ви наполягаєте на використанні JSON, ви можете перевірити jsonpickleмодуль, який є цікавим гібридним підходом.

https://github.com/jsonpickle/jsonpickle


9
Основна проблема, яку я бачу з солінням, полягає в тому, що це специфічний для Python формат, тоді як JSON - формат, незалежний від платформи. JSON особливо корисний, якщо ви пишете веб-додаток або бекенд для якогось мобільного додатку. Це було сказано, дякую, що вказали на jsonpickle.
Haroldo_OK

@Haroldo_OK Хіба jsonpickle все ще не експортує в JSON, просто не дуже читаний для людини?
Caelum

4

Ось дві прості функції для серіалізації будь-яких не складних класів, нічого фантазійного, як пояснено раніше.

Я використовую це для елементів конфігурації, тому що я можу додавати нових членів до класів без коригування коду.

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}

3

Є кілька хороших відповідей, як почати робити це. Але слід пам’ятати про деякі речі:

  • Що робити, якщо екземпляр вкладений у велику структуру даних?
  • Що робити, якщо також хочеться назву класу?
  • Що робити, якщо ви хочете десаріалізувати примірник?
  • Що робити, якщо ви використовуєте __slots__замість __dict__?
  • Що робити, якщо ви просто не хочете робити це самостійно?

json-трюки - це бібліотека (яку я створив та до якої сприяли інші), яка змогла це зробити досить довгий час. Наприклад:

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)

Ви отримаєте свій примірник назад. Ось json виглядає так:

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

Якщо ви хочете зробити власне рішення, можете поглянути на джерело, json-tricksщоб не забути деякі особливі випадки (наприклад __slots__).

Він також робить інші типи, такі як нумеровані масиви, дати, складні числа; це також дозволяє коментувати.


3

Python3.x

Найкращий підхід, якого я міг досягти зі своїми знаннями, це такий.
Зауважте, що цей код трактує також і set ()
Цей підхід є загальним, що просто потребує розширення класу (у другому прикладі).
Зауважте, що я просто роблю це у файлах, але легко змінити поведінку на свій смак.

Однак це CoDec.

Трохи більше роботи ви можете побудувати свій клас іншими способами. Я припускаю конструктор за замовчуванням, щоб застосувати його, тоді я оновлюю клас клас.

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)

Редагувати

З деякими іншими дослідженнями я знайшов спосіб узагальнити без необхідності виклику методу реєстрації SUPERCLASS , використовуючи метаклас

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

2

Я вважаю, що замість спадкування, як це пропонується у прийнятій відповіді, краще використовувати поліморфізм. Інакше вам доведеться мати велике, якщо інше твердження, щоб налаштувати кодування кожного об'єкта. Це означає створити загальний кодер за замовчуванням для JSON як:

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

а потім мати jsonEnc()функцію в кожному класі, який ви хочете серіалізувати. напр

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

Тоді ти дзвониш json.dumps(classInstance,default=jsonDefEncoder)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.