Делаем объект 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__ поле. Есть ли что-то подобное в питоне?

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

5 ответов

Решение

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

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

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

Модуль: make_json_serializable.py

""" Module that monkey-patches json module when it's 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 аргумент функции на любом json.loads() звонки, которые искали '_python_object' введите в словарь. Что-то вроде:

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(str(dct['_python_object']))
    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 от ответа Raymond Hettinger на другой вопрос сериализации JSON, который я считаю исключительно заслуживающим доверия, а также официальным источником (как в случае разработчика ядра Python).

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

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

(Хотя в 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):
    if '_python_object' in dct:
        return pickle.loads(dct['_python_object'].encode('latin1'))
    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

Вы можете расширить класс 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>>"
}

Я предлагаю положить взлом в определение класса. Таким образом, после определения класса он поддерживает 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 ]

Проблема с переопределением 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)

Для производственной среды подготовьте собственный модуль 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 вхождения.

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

редактировать:

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

Другие вопросы по тегам