Сериализация экземпляра класса в 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 (комментируя, так как ответ слишком
— наилучшие пожелания

Ответы:


238

Основная проблема заключается в том, что кодировщик 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__) - просто {}.
— Ферхан

6
Это потому, что у вашего класса нет .__init__()функции-метода, поэтому у вашего экземпляра класса есть пустой словарь. Другими словами, {}это правильный результат для вашего примера кода.
— Steveha

3
Спасибо. Это делает трюк. Я добавил простой init без параметров, и теперь вызов json.dumps (t .__ dict__) возвращает правильные данные в формате: {"value2": "345", "value1": "123"}. Я видел такие посты, как это раньше, я не был уверен, нужен ли мне специальный сериализатор для членов, необходимость init не упоминалась явно, или я пропустил это. Спасибо.
— Ферхан

3
Эта работа для одного класса, но не с соответствующими классами предметов
— Nwawel A Iroume

2
@NwawelAIroume: правда. Если у вас есть объект, который, например, содержит несколько объектов в списке, ошибка по-прежнемуis not JSON serializable
— gies0r

57

Есть один способ, который отлично работает для меня, который вы можете попробовать:

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

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__

Первые два if предназначены для сериализации даты и времени, а затем есть 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))
— Дакота Хокинс

@ Дакота хороший обходной путь; datetime.dateявляется реализацией C, следовательно, она не имеет __dict__атрибута. ИМХО ради единообразия, datetime.dateдолжно быть это ...
— codeman48

22

Я просто:

data=json.dumps(myobject.__dict__)

Это не полный ответ, и если у вас есть какой-то сложный объектный класс, вы, конечно, не получите все. Однако я использую это для некоторых из моих простых объектов.

Тот, на котором он действительно хорошо работает - это класс «options», который вы получаете из модуля 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
Основная проблема, которую я вижу в pickle, заключается в том, что это специфичный для 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-tricks - это библиотека (которую я создал, и другие внесли свой вклад), которая смогла сделать это довольно давно. Например:

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__).

Это также делает другие типы как массивы числа, datetime, комплексные числа; это также позволяет для комментариев.


3

Python3.x

Наилучшим подходом, которого я мог достичь с помощью моих знаний, было это.
Обратите внимание, что этот код также обрабатывает set ().
Этот подход является общим, просто требуется расширение класса (во втором примере).
Обратите внимание, что я просто делаю это с файлами, но легко изменить поведение на свой вкус.

Однако это кодек.

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

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

Я считаю, что вместо наследования, как предлагается в принятом ответе, лучше использовать полиморфизм. В противном случае вы должны иметь большой оператор if else для настройки кодировки каждого объекта. Это означает, что создайте общий кодировщик по умолчанию для 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.