Создание объекта 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
.