Как 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 назад. Если вы хотите сохранить типы, вам нужно написать собственный кодировщик.