Создание объекта JSON, сериализуемого с помощью обычного кодировщика

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

Обычно это выглядит так:

class CustomEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, foo):
            return obj.to_json()

        return json.JSONEncoder.default(self, obj)

print json.dumps(obj, cls = CustomEncoder)

То, что я пытаюсь сделать, состоит в том, чтобы сделать что-то сериализуемое с помощью кодировщика по умолчанию. Я огляделся, но ничего не мог найти. Я думал, что будет какое-то поле, в котором кодер будет искать json-кодировку. Нечто похожее на __str__. Возможно, поле __json__. Есть что-то подобное в python?

Я хочу сделать один класс модуля, который я делаю, чтобы быть JSON-сериализуемым для всех, кто использует пакет, не беспокоясь о том, чтобы реализовать свои собственные [тривиальные] пользовательские кодеры.

Ответ 1

Как я сказал в комментарии к вашему вопросу, после просмотра исходного кода модуля json он, похоже, не может выполнять то, что вы хотите. Однако цель может быть достигнута так называемым исправлением обезьяны (см. Вопрос Что такое исправление обезьяны?). Это можно сделать в сценарии инициализации вашего пакета __init__.py и повлиять на всю последующую сериализацию модулей json поскольку модули обычно загружаются только один раз, а результат кэшируется в sys.modules.

Патч изменяет default метод кодировщика json по default - default по default().

Вот пример, реализованный в виде отдельного модуля для простоты:

Модуль: make_json_serializable.py

""" Module that monkey-patches json module when it imported so
JSONEncoder.default() automatically checks for a special "to_json()"
method and uses it to encode the object if found.
"""
from json import JSONEncoder

def _default(self, obj):
    return getattr(obj.__class__, "to_json", _default.default)(obj)

_default.default = JSONEncoder.default  # Save unmodified default.
JSONEncoder.default = _default # Replace it.

Использовать его тривиально, так как патч применяется путем простого импорта модуля.

Пример клиентского скрипта:

import json
import make_json_serializable  # apply monkey-patch

class Foo(object):
    def __init__(self, name):
        self.name = name
    def to_json(self):  # New special method.
        """ Convert to JSON format string representation. """
        return '{"name": "%s"}' % self.name

foo = Foo('sazpaz')
print(json.dumps(foo))  # -> "{\"name\": \"sazpaz\"}"

Чтобы сохранить информацию о типе объекта, специальный метод также может включить ее в возвращаемую строку:

        return ('{"type": "%s", "name": "%s"}' %
                 (self.__class__.__name__, self.name))

Который производит следующий JSON, который теперь включает имя класса:

"{\"type\": \"Foo\", \"name\": \"sazpaz\"}"

Здесь лежит магия

Даже лучше, чем при замене default() искать метод со специальным именем, он сможет автоматически сериализовать большинство объектов Python, включая пользовательские экземпляры классов, без необходимости добавления специального метода. После исследования ряда альтернатив, следующие, которые используют модуль pickle, показались мне наиболее близкими к этому идеалу:

Модуль: make_json_serializable2.py

""" Module that imports the json module and monkey-patches it so
JSONEncoder.default() automatically pickles any Python objects
encountered that aren't standard JSON data types.
"""
from json import JSONEncoder
import pickle

def _default(self, obj):
    return {'_python_object': pickle.dumps(obj)}

JSONEncoder.default = _default  # Replace with the above.

Конечно, нельзя все засолить, например, типы расширения. Однако существуют определенные способы обработки их с помощью протокола Pickle путем написания специальных методов - аналогично тому, что вы предложили и я описывал ранее, - но это, вероятно, будет необходимо для гораздо меньшего числа случаев.

Несмотря на это, использование протокола pickle также означает, что было бы довольно легко восстановить исходный объект Python, предоставив настраиваемый object_hook функции object_hook для любых json.loads() которые использовали любой ключ '_python_object' в передаваемом словаре, всякий раз, когда он имеет один. Что-то вроде:

def as_python_object(dct):
    try:
        return pickle.loads(str(dct['_python_object']))
    except KeyError:
        return dct

pyobj = json.loads(json_str, object_hook=as_python_object)

Если это нужно сделать во многих местах, возможно, стоит определить функцию-оболочку, которая автоматически предоставит дополнительный аргумент ключевого слова:

json_pkloads = functools.partial(json.loads, object_hook=as_python_object)

pyobj = json_pkloads(json_str)

Естественно, это можно было бы пропатчить обезьяной в модуль json, сделав функцию по умолчанию object_hook (вместо None).

У меня появилась идея использовать pickle из ответа Рэймонда Хеттингера на другой вопрос сериализации JSON, который я считаю исключительно заслуживающим доверия, а также официальным источником (как у разработчика ядра Python).

Переносимость на Python 3

Код выше не работает, как показано на Python 3, поскольку json.dumps() возвращает bytes объект, который JSONEncoder не может справиться. Однако этот подход все еще действует. Простой способ обойти эту проблему заключается в том, чтобы latin1 "декодировал" значение, возвращаемое из pickle.dumps() а затем "кодировал" его из latin1 прежде чем передать его в pickle.loads() в функции as_python_object(). Это работает, потому что произвольные двоичные строки являются допустимыми значениями latin1 которые всегда могут быть декодированы в Unicode, а затем снова закодированы обратно в исходную строку (как указано в этом ответе Свеном Марнахом).

(Хотя в Python 2 хорошо работает следующее, декодирование и кодирование в latin1 излишне.)

from decimal import Decimal

class PythonObjectEncoder(json.JSONEncoder):
    def default(self, obj):
        return {'_python_object': pickle.dumps(obj).decode('latin1')}

def as_python_object(dct):
    try:
        return pickle.loads(dct['_python_object'].encode('latin1'))
    except KeyError:
        return dct

data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'},
        Decimal('3.14')]
j = json.dumps(data, cls=PythonObjectEncoder, indent=4)
data2 = json.loads(j, object_hook=as_python_object)
assert data == data2  # both should be same

Ответ 2

Вы можете расширить класс dict следующим образом:

#!/usr/local/bin/python3
import json

class Serializable(dict):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # hack to fix _json.so make_encoder serialize properly
        self.__setitem__('dummy', 1)

    def _myattrs(self):
        return [
            (x, self._repr(getattr(self, x))) 
            for x in self.__dir__() 
            if x not in Serializable().__dir__()
        ]

    def _repr(self, value):
        if isinstance(value, (str, int, float, list, tuple, dict)):
            return value
        else:
            return repr(value)

    def __repr__(self):
        return '<%s.%s object at %s>' % (
            self.__class__.__module__,
            self.__class__.__name__,
            hex(id(self))
        )

    def keys(self):
        return iter([x[0] for x in self._myattrs()])

    def values(self):
        return iter([x[1] for x in self._myattrs()])

    def items(self):
        return iter(self._myattrs())

Теперь, чтобы сделать ваши классы сериализуемыми с обычным кодировщиком, расширьте "Serializable":

class MySerializableClass(Serializable):

    attr_1 = 'first attribute'
    attr_2 = 23

    def my_function(self):
        print('do something here')


obj = MySerializableClass()

print(obj) напечатает что-то вроде:

<__main__.MySerializableClass object at 0x1073525e8>

print(json.dumps(obj, indent=4)) напечатает что-то вроде:

{
    "attr_1": "first attribute",
    "attr_2": 23,
    "my_function": "<bound method MySerializableClass.my_function of <__main__.MySerializableClass object at 0x1073525e8>>"
}

Ответ 3

Я предлагаю вставить определение класса в определение класса. Таким образом, после определения класса он поддерживает JSON. Пример:

import json

class MyClass( object ):

    def _jsonSupport( *args ):
        def default( self, xObject ):
            return { 'type': 'MyClass', 'name': xObject.name() }

        def objectHook( obj ):
            if 'type' not in obj:
                return obj
            if obj[ 'type' ] != 'MyClass':
                return obj
            return MyClass( obj[ 'name' ] )
        json.JSONEncoder.default = default
        json._default_decoder = json.JSONDecoder( object_hook = objectHook )

    _jsonSupport()

    def __init__( self, name ):
        self._name = name

    def name( self ):
        return self._name

    def __repr__( self ):
        return '<MyClass(name=%s)>' % self._name

myObject = MyClass( 'Magneto' )
jsonString = json.dumps( [ myObject, 'some', { 'other': 'objects' } ] )
print "json representation:", jsonString

decoded = json.loads( jsonString )
print "after decoding, our object is the first in the list", decoded[ 0 ]

Ответ 4

Проблема с переопределением JSONEncoder().default заключается в том, что вы можете сделать это только один раз. Если вы наткнетесь на какой-либо специальный тип данных, который не работает с этим шаблоном (например, если вы используете странную кодировку). С приведенным ниже шаблоном вы всегда можете сделать сериализуемый класс JSON класса, при условии, что поле класса, которое вы хотите сериализовать, сериализуется сам (и может быть добавлено в список python, практически ничего). В противном случае вы должны применить рекурсивно один и тот же шаблон к своему json-полю (или извлечь из него сериализуемые данные):

# base class that will make all derivatives JSON serializable:
class JSONSerializable(list): # need to derive from a serializable class.

  def __init__(self, value = None):
    self = [ value ]

  def setJSONSerializableValue(self, value):
    self = [ value ]

  def getJSONSerializableValue(self):
    return self[1] if len(self) else None


# derive  your classes from JSONSerializable:
class MyJSONSerializableObject(JSONSerializable):

  def __init__(self): # or any other function
    # .... 
    # suppose your__json__field is the class member to be serialized. 
    # it has to be serializable itself. 
    # Every time you want to set it, call this function:
    self.setJSONSerializableValue(your__json__field)
    # ... 
    # ... and when you need access to it,  get this way:
    do_something_with_your__json__field(self.getJSONSerializableValue())


# now you have a JSON default-serializable class:
a = MyJSONSerializableObject()
print json.dumps(a)

Ответ 5

Я не понимаю, почему вы не можете написать функцию serialize для своего собственного класса? Вы реализуете собственный кодировщик внутри самого класса и позволяете "людям" вызывать функцию сериализации, которая по существу возвращает self.__dict__ с отключенными функциями.

изменить:

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

Ответ 6

Для продакшен среды подготовьте собственный модуль json со своим собственным кодировщиком, чтобы было ясно, что вы что-то переопределяете. Monkey-patch не рекомендуется, но вы можете сделать monkey patch в вашем тесте.

Например,

class JSONDatetimeAndPhonesEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, (datetime.date, datetime.datetime)):
            return obj.date().isoformat()
        elif isinstance(obj, basestring):
            try:
                number = phonenumbers.parse(obj)
            except phonenumbers.NumberParseException:
                return json.JSONEncoder.default(self, obj)
            else:
                return phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.NATIONAL)
        else:
            return json.JSONEncoder.default(self, obj)

ты хочешь:

payload = json.dumps(your_data, cls = JSONDatetimeAndPhonesEncoder)

или же:

payload = your_dumps (your_data)

или же:

payload = your_json.dumps(your_data)

Однако в тестовой среде идите головой:

@pytest.fixture(scope='session', autouse=True)
def testenv_monkey_patching():
    json._default_encoder = JSONDatetimeAndPhonesEncoder()

который будет применять ваш кодировщик ко всем вхождениям json.dumps.