Как JSON сериализовать наборы?

149

У меня есть 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 в качестве источника данных, поэтому даты, похоже, тоже вызывают эту ошибку)

Любой намек в правильном направлении будет оценен.

РЕДАКТИРОВАТЬ:

Спасибо за ответ! Возможно, мне следовало быть более точным.

Я использовал (и проголосовал) ответы здесь, чтобы обойти ограничения setпереводимого, но есть и внутренние ключи, которые также являются проблемой.

Объекты в setявляются сложными объектами, которые преобразуются __dict__, но сами они также могут содержать значения своих свойств, которые могут не подходить для базовых типов в кодировщике json.

В него входит много разных типов set, и хеш в основном вычисляет уникальный идентификатор для сущности, но в истинном духе NoSQL точно не сказано, что содержит дочерний объект.

Один объект может содержать значение даты для starts, тогда как другой может иметь некоторую другую схему, которая не содержит ключей, содержащих «не примитивные» объекты.

Вот почему единственное решение, которое я мог придумать, - это расширение метода JSONEncoderзамены defaultдля включения разных случаев, но я не уверен, как это сделать, и документация неоднозначна. Во вложенных объектах, значение, возвращаемое при defaultпереходе по ключу, или это просто общее включение / отбрасывание, которое смотрит на весь объект? Как этот метод учитывает вложенные значения? Я просмотрел предыдущие вопросы и, похоже, не могу найти лучший подход к кодированию для конкретного случая (что, к сожалению, похоже на то, что мне нужно сделать здесь).

DeaconDesperado
источник
3
почему dictс? Я думаю, что вы хотите сделать listиз набора только что, а затем передать его кодировщику ... например:encode(list(myset))
Константин
2
Вместо использования JSON вы можете использовать YAML (JSON по сути является подмножеством YAML).
Паоло Моретти
@PaoloMoretti: Это приносит хоть какое-то преимущество? Я не думаю, что наборы входят в число универсально поддерживаемых типов данных YAML, и они не так широко поддерживаются, особенно в отношении API.
@PaoloMoretti Спасибо за ваш вклад, но для внешнего интерфейса приложения требуется JSON как тип возвращаемого значения, и это требование для всех целей исправлено.
DeaconDesperado
2
@delnan Я предлагал YAML, потому что он имеет встроенную поддержку как наборов, так и дат .
Паоло Моретти

Ответы:

117

Нотация 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 . Каждый из них поддерживает гораздо больший диапазон типов данных.

Раймонд Хеттингер
источник
11
Это первый раз, когда я услышал, что YAML более общего назначения, чем JSON ... o_O
Карл Кнехтель
13
@KarlKnechtel YAML - это расширенный набор JSON (почти). Он также добавляет теги для двоичных данных, наборов, упорядоченных карт и отметок времени. Поддержка большего количества типов данных - это то, что я имел в виду под «более общим назначением». Вы, кажется, используете фразу «общего назначения» в другом смысле.
Рэймонд Хеттингер
4
Не забывайте также о jsonpickle , который призван стать обобщенной библиотекой для выбора объектов Python в JSON, как и предполагает этот ответ.
Джейсон Р. Кумбс
4
Начиная с версии 1.2, YAML является строгим надмножеством JSON. Все легальные JSON сейчас являются легальными YAML. yaml.org/spec/1.2/spec.html
steveha
2
этот пример кода импортирует, JSONDecoderно не использует его
watsonic
115

Вы можете создать собственный кодировщик, который возвращает, 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"]'
jterrace
источник
Спасибо, я отредактировал вопрос, чтобы лучше указать, что это именно то, что мне нужно. Я не могу понять, как этот метод будет обрабатывать вложенные объекты. В вашем примере возвращаемое значение - список для набора, но что, если переданный объект был набором с датами (еще один неправильный тип данных) внутри него? Должен ли я сверлить ключи в самом методе по умолчанию? Благодаря тонну!
DeaconDesperado
1
Я думаю, что модуль JSON обрабатывает вложенные объекты для вас. Как только он вернет список, он будет перебирать элементы списка, пытаясь закодировать каждый из них. Если одна из них является датой, defaultфункция будет вызвана снова, на этот раз с objиспользованием объекта даты, поэтому вам просто нужно проверить ее и вернуть представление даты.
jterrace
Таким образом, метод по умолчанию может быть запущен несколько раз для любого переданного ему объекта, так как он также будет смотреть на отдельные ключи после того, как он будет «прослушан»?
DeaconDesperado
В некотором смысле, он не будет вызываться несколько раз для одного и того же объекта, но может перерасти в потомков. Смотрите обновленный ответ.
jterrace
Работал именно так, как вы описали. Я все еще должен выяснить некоторые недостатки, но большинство из них, вероятно, могут быть подвергнуты рефакторингу. Большое спасибо за ваше руководство!
DeaconDesperado
7

Я адаптировал решение 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')]
simlmx
источник
4
Код, показанный в конце этого ответа на связанный вопрос, выполняет то же самое путем [только] декодирования и кодирования объекта байтов, json.dumps()возвращаемого в / из 'latin1', пропуская ненужные base64вещи.
Мартино
6

В словаре JSON доступны только словари, списки и типы примитивных объектов (int, string, bool).

Жозеф Ле Брех
источник
5
«Примитивный тип объекта» не имеет смысла, когда речь идет о Python. «Встроенный объект» имеет больше смысла, но здесь он слишком широк (для начала: он включает в себя диктовки, списки, а также наборы). (Хотя терминология JSON может отличаться.)
строка номер объекта массив истина ложь нуль
Джозеф Ле Брех
6

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

Антти Хаапала
источник
4

Если вам нужно только кодировать наборы, а не общие объекты 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
NeilenMarais
источник
1

Если вам нужен просто быстрый дамп и вы не хотите реализовывать пользовательский кодировщик. Вы можете использовать следующее:

json_string = json.dumps(data, iterable_as_array=True)

Это преобразует все наборы (и другие итерируемые элементы) в массивы. Просто помните, что эти поля останутся массивами, когда вы проанализируете JSON. Если вы хотите сохранить типы, вам нужно написать собственный кодировщик.

Дэвид Новак
источник
7
Когда я пытаюсь это сделать, я получаю: TypeError: __init __ () получил неожиданный аргумент ключевого слова 'iterable_as_array'
atm
Вам нужно установить simplejson
JerryBringer
импорт simplejson как json, а затем json_string = json.dumps (data, iterable_as_array = True) хорошо работает в Python 3.6
fraverta
1

Одним из недостатков принятого решения является то, что его вывод очень специфичен для 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ключом. Очевидно, вы можете использовать другой, более глубоко запутанный ключ.

sagism
источник