Как JSON сериализовать наборы?
У меня есть питон 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 в качестве источника данных, поэтому даты, похоже, тоже вызывают эту ошибку)
Любой намек в правильном направлении будет оценен.
РЕДАКТИРОВАТЬ:
Спасибо за ответ! Возможно, мне следовало быть более точным.
Я использовал (и проголосовал) ответы здесь, чтобы обойти ограничения set
переводится, но есть внутренние ключи, которые также являются проблемой.
Объекты в set
сложные объекты, которые переводятся в __dict__
, но сами они также могут содержать значения для своих свойств, которые могут не подходить для базовых типов в кодировщике json.
Есть много разных типов, входящих в это set
и хеш в основном вычисляет уникальный идентификатор для сущности, но в истинном духе NoSQL точно не сказано, что содержит дочерний объект.
Один объект может содержать значение даты для starts
тогда как другая может иметь некоторую другую схему, которая не содержит ключей, содержащих "не примитивные" объекты.
Вот почему единственное решение, которое я мог придумать, это расширить JSONEncoder
заменить default
метод включения разных случаев - но я не уверен, как это сделать, и документация неоднозначна. Во вложенных объектах, возвращается ли значение из default
идти по ключу, или это просто общее включение / сброс, который смотрит на весь объект? Как этот метод учитывает вложенные значения? Я просмотрел предыдущие вопросы и, похоже, не могу найти лучший подход к кодированию для конкретного случая (что, к сожалению, похоже на то, что мне нужно сделать здесь).
12 ответов
В нотации JSON есть только несколько собственных типов данных (объекты, массивы, строки, числа, логические значения и ноль), поэтому все, что сериализовано в JSON, должно быть выражено как один из этих типов.
Как показано в документации по json-модулю, это преобразование может выполняться автоматически JSONEncoder и JSONDecoder, но тогда вы отказываетесь от какой-то другой структуры, которая может вам понадобиться (если вы преобразуете наборы в список, вы теряете возможность регулярно восстанавливать списки, если вы преобразуете наборы в словарь, используя dict.fromkeys(s)
тогда вы теряете возможность восстанавливать словари).
Более сложным решением является создание пользовательского типа, который может сосуществовать с другими собственными типами JSON. Это позволяет хранить вложенные структуры, которые включают списки, наборы, дикты, десятичные дроби, объекты даты и времени и т. Д.:
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
Вот пример сеанса, показывающий, что он может обрабатывать списки, запросы и наборы:
>>> 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. Каждый из них поддерживает гораздо больший диапазон типов данных.
Вы можете создать собственный кодировщик, который возвращает 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"]'
Вам не нужно создавать собственный класс кодировщика для предоставления default
метод - его можно передать как аргумент ключевого слова:
import json
def serialize_sets(obj):
if isinstance(obj, set):
return list(obj)
return obj
json_str = json.dumps(set([1,2,3]), default=serialize_sets)
print(json_str)
приводит к [1, 2, 3]
во всех поддерживаемых версиях Python.
Если вы точно знаете, что единственными несериализуемыми данными будут s, есть очень простое (и грязное) решение:
json.dumps({"Hello World": {1, 2}}, default=tuple)
Только несериализуемые данные будут обрабатываться функцией, заданной как
default
, так что только
set
будет преобразован в
tuple
.
Я адаптировал решение Raymond Hettinger для 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')]
Если вам нужен просто быстрый дамп и вы не хотите реализовывать пользовательский кодировщик. Вы можете использовать следующее:
json_string = json.dumps(data, iterable_as_array=True)
Это преобразует все наборы (и другие итерируемые элементы) в массивы. Просто помните, что эти поля останутся массивами, когда вы проанализируете JSON. Если вы хотите сохранить типы, вам нужно написать собственный кодировщик.
Укороченная версия @AnttiHaapala:
json.dumps(dict_with_sets, default=lambda x: list(x) if isinstance(x, set) else x)
В словаре JSON доступны только словари, списки и типы примитивных объектов (int, string, bool).
Если вам нужно только кодировать наборы, а не общие объекты 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
>>> import json
>>> set_object = set([1,2,3,4])
>>> json.dumps(list(set_object))
'[1, 2, 3, 4]'
Одним из недостатков принятого решения является то, что его вывод очень специфичен для Python. Т.е. его необработанный вывод json не может быть просмотрен человеком или загружен другим языком (например, javascript). пример:
db = {
"a": [ 44, set((4,5,6)) ],
"b": [ 55, set((4,3,2)) ]
}
j = dumps(db, cls=PythonObjectEncoder)
print(j)
Вы получите:
{"a": [44, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsESwVLBmWFcQJScQMu"}], "b": [55, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsCSwNLBGWFcQJScQMu"}]}
Я могу предложить решение, которое понижает рейтинг набора до dict, содержащего список на выходе, и обратно до набора при загрузке в python с использованием того же кодировщика, тем самым сохраняя наблюдаемость и языковой агностицизм:
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)
elif isinstance(obj, set):
return {"__set__": list(obj)}
return {'_python_object': b64encode(pickle.dumps(obj)).decode('utf-8')}
def as_python_object(dct):
if '__set__' in dct:
return set(dct['__set__'])
elif '_python_object' in dct:
return pickle.loads(b64decode(dct['_python_object'].encode('utf-8')))
return dct
db = {
"a": [ 44, set((4,5,6)) ],
"b": [ 55, set((4,3,2)) ]
}
j = dumps(db, cls=PythonObjectEncoder)
print(j)
ob = loads(j)
print(ob["a"])
Что дает вам:
{"a": [44, {"__set__": [4, 5, 6]}], "b": [55, {"__set__": [2, 3, 4]}]}
[44, {'__set__': [4, 5, 6]}]
Обратите внимание, что сериализация словаря, в котором есть элемент с ключом"__set__"
сломает этот механизм. Так__set__
теперь стал зарезервированным dict
ключ. Очевидно, не стесняйтесь использовать другой, более глубоко запутанный ключ.
вы должны попробовать jsonwhatever
https://pypi.org/project/jsonwhatever/
pip установить jsonwhatever
from jsonwhatever import JsonWhatEver
set_a = {1,2,3}
jsonwe = JsonWhatEver()
string_res = jsonwe.jsonwhatever('set_string', set_a)
print(string_res)