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