Создание сериализуемого объекта 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 для всех, кто использует пакет, не беспокоясь о реализации своих собственных [тривиальных] пользовательских кодеров.

5 51

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 С ответ by Компания Raymond Hettinger к другому вопросу сериализации JSON, который я считаю исключительно надежным, а также официальным источником (как в Python core developer).

Portablity на Python 3

приведенный выше код не работает, как показано в Python 3, потому что json.dumps() возвращает

вы можете расширить класс 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())

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

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)

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

edit:

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