Как 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)
Другие вопросы по тегам