Как JSON сериализует множество?
У меня есть Python set
, который содержит объекты с методами __hash__
и __eq__
, чтобы не включать в коллекцию определенные дубликаты.
Мне нужно json закодировать этот результат set
, но передача даже пустого метода set
в json.dumps
вызывает TypeError
.
File "/usr/lib/python2.7/json/encoder.py", line 201, in encode
chunks = self.iterencode(o, _one_shot=True)
File "/usr/lib/python2.7/json/encoder.py", line 264, in iterencode
return _iterencode(o, 0)
File "/usr/lib/python2.7/json/encoder.py", line 178, in default
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: set([]) is not JSON serializable
Я знаю, что могу создать расширение класса json.JSONEncoder
, у которого есть собственный метод default
, но я даже не уверен, с чего начать преобразование по set
. Должен ли я создавать словарь из значений set
в методе по умолчанию, а затем возвращать кодировку? В идеале, я бы хотел, чтобы метод по умолчанию мог обрабатывать все типы данных, которые заглушает исходный кодер (я использую Mongo в качестве источника данных, поэтому даты, похоже, тоже повышают эту ошибку)
Было бы понятно любой намек в правильном направлении.
EDIT:
Спасибо за ответ! Возможно, я должен был быть более точным.
Я использовал (и подтвердил) ответы здесь, чтобы обойти ограничения перевода set
, но есть и внутренние ключи, которые также являются проблемой.
Объекты в set
являются сложными объектами, которые преобразуются в __dict__
, но сами они также могут содержать значения для своих свойств, которые могут быть непригодны для основных типов в кодировщике json.
В этот set
существует много разных типов, и хэш в основном вычисляет уникальный идентификатор для сущности, но в истинном духе NoSQL не сообщается точно, что содержит дочерний объект.
Один объект может содержать значение даты для starts
, тогда как другое может иметь другую схему, которая не содержит ключей, содержащих "непримитивные" объекты.
Вот почему единственным решением, которое я мог подумать, было расширение JSONEncoder
, чтобы заменить метод default
, чтобы включить разные случаи, но я не уверен, как это сделать, а документация неоднозначна. Во вложенных объектах значение, возвращаемое из default
, идет по ключу, или это просто общий include/discard, который смотрит на весь объект? Как этот метод поддерживает вложенные значения? Я просмотрел предыдущие вопросы и не могу найти лучший подход к кодировке, зависящей от конкретного случая (что, к сожалению, похоже на то, что мне нужно будет делать здесь).
Ответы
Ответ 1
JSON нотация имеет только несколько собственных типов данных (объекты, массивы, строки, числа, логические и нулевые), поэтому ничего сериализованный в JSON, должен быть выражен как один из этих типов.
Как показано в json module docs, это преобразование может быть выполнено автоматически с помощью JSONEncoder и JSONDecoder, но тогда вы бы отказались от некоторых (если вы конвертируете наборы в список, то вы теряете возможность восстановления обычных списков, если вы конвертируете наборы в словарь с помощью dict.fromkeys(s)
, то теряете возможность восстановления словарей).
Более сложное решение - это создание настраиваемого типа, который может сосуществовать с другими родными типами JSON. Это позволяет хранить вложенные структуры, которые включают в себя списки, наборы, dicts, десятичные знаки, объекты datetime и т.д.:
from json import dumps, loads, JSONEncoder, JSONDecoder
import pickle
class PythonObjectEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, (list, dict, str, unicode, int, float, bool, type(None))):
return JSONEncoder.default(self, obj)
return {'_python_object': pickle.dumps(obj)}
def as_python_object(dct):
if '_python_object' in dct:
return pickle.loads(str(dct['_python_object']))
return dct
Вот пример сеанса, показывающий, что он может обрабатывать списки, dicts и sets:
>>> data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]
>>> j = dumps(data, cls=PythonObjectEncoder)
>>> loads(j, object_hook=as_python_object)
[1, 2, 3, set(['knights', 'say', 'who', 'ni']), {u'key': u'value'}, Decimal('3.14')]
В качестве альтернативы может оказаться полезным использовать более универсальный метод сериализации, такой как YAML, Twisted Jelly, или Python pickle module. Каждый из них поддерживает гораздо больший диапазон типов данных.
Ответ 2
Вы можете создать собственный кодер, который возвращает list
, когда он встречает set
. Вот пример:
>>> import json
>>> class SetEncoder(json.JSONEncoder):
... def default(self, obj):
... if isinstance(obj, set):
... return list(obj)
... return json.JSONEncoder.default(self, obj)
...
>>> json.dumps(set([1,2,3,4,5]), cls=SetEncoder)
'[1, 2, 3, 4, 5]'
Вы также можете обнаружить другие типы. Если вам нужно сохранить, что список фактически является набором, вы можете использовать пользовательскую кодировку. Возможно, что-то вроде return {'type':'set', 'list':list(obj)}
.
Чтобы проиллюстрировать вложенные типы, рассмотрите сериализацию этого:
>>> class Something(object):
... pass
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)
Это вызывает следующую ошибку:
TypeError: <__main__.Something object at 0x1691c50> is not JSON serializable
Это означает, что кодер примет возвращаемый результат list
и рекурсивно вызовет сериализатор на своих дочерних элементах. Чтобы добавить собственный сериализатор для нескольких типов, вы можете сделать это:
>>> class SetEncoder(json.JSONEncoder):
... def default(self, obj):
... if isinstance(obj, set):
... return list(obj)
... if isinstance(obj, Something):
... return 'CustomSomethingRepresentation'
... return json.JSONEncoder.default(self, obj)
...
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)
'[1, 2, 3, 4, 5, "CustomSomethingRepresentation"]'
Ответ 3
В JSON доступны только словари, списки и примитивные типы объектов (int, string, bool).
Ответ 4
Я адаптировал решение Раймонда Хеттингера на python 3.
Вот что изменилось:
-
unicode
исчез
- обновил вызов родителям
default
с помощью super()
- с помощью
base64
для сериализации типа bytes
в str
(поскольку кажется, что bytes
в python 3 нельзя преобразовать в JSON)
from decimal import Decimal
from base64 import b64encode, b64decode
from json import dumps, loads, JSONEncoder
import pickle
class PythonObjectEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, (list, dict, str, int, float, bool, type(None))):
return super().default(obj)
return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}
def as_python_object(dct):
if '_python_object' in dct:
return pickle.loads(b64decode(dct['_python_object'].encode('utf-8')))
return dct
data = [1,2,3, set(['knights', 'who', 'say', 'ni']), {'key':'value'}, Decimal('3.14')]
j = dumps(data, cls=PythonObjectEncoder)
print(loads(j, object_hook=as_python_object))
# prints: [1, 2, 3, {'knights', 'who', 'say', 'ni'}, {'key': 'value'}, Decimal('3.14')]
Ответ 5
Если вам нужно только закодировать наборы, а не общие объекты Python, и хотите, чтобы он был легко читаемым человеком, можно использовать упрощенную версию ответа Раймонда Хеттингера:
import json
import collections
class JSONSetEncoder(json.JSONEncoder):
"""Use with json.dumps to allow Python sets to be encoded to JSON
Example
-------
import json
data = dict(aset=set([1,2,3]))
encoded = json.dumps(data, cls=JSONSetEncoder)
decoded = json.loads(encoded, object_hook=json_as_python_set)
assert data == decoded # Should assert successfully
Any object that is matched by isinstance(obj, collections.Set) will
be encoded, but the decoded value will always be a normal Python set.
"""
def default(self, obj):
if isinstance(obj, collections.Set):
return dict(_set_object=list(obj))
else:
return json.JSONEncoder.default(self, obj)
def json_as_python_set(dct):
"""Decode json {'_set_object': [1,2,3]} to set([1,2,3])
Example
-------
decoded = json.loads(encoded, object_hook=json_as_python_set)
Also see :class:`JSONSetEncoder`
"""
if '_set_object' in dct:
return set(dct['_set_object'])
return dct
Ответ 6
Если вам нужен просто быстрый дамп и вы не хотите внедрять пользовательский кодер. Вы можете использовать следующее:
json_string = json.dumps(data, iterable_as_array=True)
Это преобразует все множества (и другие итерации) в массивы. Просто будьте осторожны, чтобы эти поля оставались массивами при анализе json назад. Если вы хотите сохранить типы, вам нужно написать собственный кодировщик.