Як серіалізувати набори 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переходу за ключем, чи це просто загальне включення / відкидання, яке переглядає весь об'єкт? Як цей метод вміщує вкладені значення? Я переглянув попередні питання і, здається, не знайшов найкращого підходу до кодування, що залежить від конкретного випадку (який, на жаль, здається, що мені тут потрібно зробити).


3
чому dicts? Думаю, ви хочете зробити просто listз набору, а потім передати його кодеру ... Наприклад:encode(list(myset))
Константіній

2
Замість використання JSON, ви можете використовувати YAML (JSON по суті є підмножиною YAML).
Паоло Моретті

@PaoloMoretti: Хоча це приносить користь? Я не думаю, що набори є одними з універсально підтримуваних типів даних YAML, і він менш широко підтримується, особливо щодо API.

@PaoloMoretti Дякую за ваш вклад, але передній додаток вимагає JSON як тип повернення, і ця вимога визначена для всіх цілей.
DeaconDesperado

2
@delnan Я пропонував YAML, оскільки він підтримує як набори, так і дати .
Паоло Моретті

Відповіді:


116

Позначення 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's pickle module . Вони підтримують набагато більший спектр типів даних.


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

Ви можете створити спеціальний кодер, який повертає a, listколи він стикається з a 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"]'

Дякую, я відредагував питання, щоб краще уточнити, що це тип речі, який мені потрібен. Я не можу зрозуміти, як цей метод поводитиметься з вкладеними об'єктами. У вашому прикладі повернене значення - це список для набору, але що робити, якщо об'єкт переданий - це набір із датами (іншим поганим типом даних) всередині нього? Чи слід просвітити ключі в рамках самого методу за замовчуванням? Дякую тонну!
DeaconDesperado

1
Я думаю, що модуль JSON обробляє вкладені вам об’єкти. Після повернення списку він повторює елементи списку, намагаючись кодувати кожен. Якщо однією з них є дата, defaultфункція буде викликана знову, на цей раз з objоб’єктом дати, тому вам просто потрібно перевірити її та повернути подання дати.
jterrace

Тож метод за замовчуванням міг би запустити кілька разів для будь-якого переданого йому об'єкта, оскільки він також буде дивитись на окремі клавіші, як тільки він буде "визначений"?
DeaconDesperado

Начебто, він не буде викликатися кілька разів для одного і того ж об'єкта, але він може повторюватися у дітей. Дивіться оновлену відповідь.
jterrace

Працювали саме так, як ви описали. Мені досі належить з'ясувати деякі несправності, але більшість це, мабуть, речі, які можна відновити. Дякую тонну за ваші вказівки!
DeaconDesperado

7

Я адаптував рішення Реймонда Хеттінгера до пітона 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')]

4
Код, показаний в кінці цієї відповіді на пов'язане запитання, виконує те саме, що [тільки] розшифровує і кодує об'єкт байтів, json.dumps()повертається в / з 'latin1', пропускаючи base64речі, які не потрібні.
мартино

6

У JSON доступні лише словники, списки та примітивні типи об'єктів (int, string, bool).


5
"Первісний тип об'єкта" не має сенсу, коли говорити про Python. "Вбудований об'єкт" має більше сенсу, але тут занадто широкий (для початку: він включає дикти, списки, а також набори). (Термінологія JSON може бути різною.)

рядок масив об'єкта масив true false null
Джозеф Ле Брех

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

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 (дані, iterable_as_array = True) добре працює в Python 3.6
fraverta

1

Одним із недоліків прийнятого рішення є те, що його вихід дуже питонний. Тобто його вихідний 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"}]}

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.