2011-11-22 3 views
97

У меня есть Python set, который содержит объекты с __hash__ и __eq__ методов, чтобы не допускать дубликатов в коллекцию.Как сериализовать сериалы JSON?

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

+2

почему 'dict's? Я думаю, что вы хотите сделать только «список» из набора, а затем передать его в кодировщик ... например: 'encode (list (myset))' – Constantinius

+1

Вместо использования JSON вы можете использовать YAML (JSON по существу подмножество YAML). –

+0

@PaoloMoretti: Приносит ли это какое-то преимущество?Я не думаю, что набор относится к универсальным типам данных YAML, и он менее широко поддерживается, особенно в отношении API. – delnan

ответ

78

JSON В нотации имеется только несколько собственных типов данных (объектов, массивов, строк, чисел, булевых и нулевых значений), поэтому все, что сериализовано в JSON, должно быть выражено как один из этих типов.

Как показано в json module docs, это преобразование может быть сделано автоматически с помощью JSONEncoder и JSONDecoder, но тогда вы бы отказаться от какой-либо другой структуры, вам может понадобиться (если преобразовать наборы в список, то вы потерять возможность восстановления регулярных списков, если вы конвертируете наборы в словарь с использованием dict.fromkeys(s), вы теряете возможность восстановления словарей).

Более сложное решение заключается в создании пользовательского типа, который может сосуществовать с другими родными типами JSON. Это позволяет сохранять вложенные структуры, которые включают в себя списки, наборы, dicts, десятичные, даты-времени объектов и т.д .:

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 и наборы:

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

+0

В вашей тестовой сессии должен ли PythonSetEncoder быть PythonObjectEncoder? –

+0

Просто убедитесь, что вы не используете это на ненадежном вводе, поскольку [pickle не предназначен для защиты от ошибочных или вредоносных данных] (http://docs.python.org/library/pickle.html), в то время как JSON - это (пока не будет настроен с рассолом). –

+7

Это первое, что я слышал о том, что YAML является более общей целью, чем JSON ... o_O –

3

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

+3

«Тип примитивного объекта» не имеет смысла, говоря о Python. «Встроенный объект» имеет больше смысла, но здесь слишком широк (для начинающих: он включает в себя дикты, списки, а также набор). (JSON терминология может отличаться, хотя.) – delnan

+0

строкой номера объекта массива истинных ложных нулевых –

67

Вы можете создать собственный кодер, который возвращает 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"]' 
+0

Спасибо, я редактировал вопрос лучше уточнить, что это был тип вещи, что мне нужно. Я не могу понять, как этот метод будет обрабатывать вложенные объекты. В вашем примере возвращаемое значение - это список для set, но что, если объект, переданный в него, был набором с датами (другой плохим типом данных) внутри него? Должен ли я прокручивать ключи внутри самого метода по умолчанию? Благодаря тонну! – DeaconDesperado

+1

Я думаю, что модуль JSON обрабатывает вложенные объекты для вас. Как только он вернет список, он будет перебирать элементы списка, пытающиеся их кодировать.Если одним из них является дата, функция '' default'' снова будет вызвана, на этот раз с '' obj'' будет объект даты, поэтому вам просто нужно проверить его и вернуть представление даты. – jterrace

+0

Таким образом, метод по умолчанию может, предположительно, запускаться несколько раз для любого переданного ему объекта, так как он также будет рассматривать отдельные ключи, как только он будет «послан»? – DeaconDesperado

3

Если вам нужно только для кодирования наборов, а не общие объектов Python, и хотите сохранить его легко читаемым человек, упрощенную версию Raymond Hettinger-х ответ может быть использован:

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 
2

Я приспособил Raymond Hettinger's solution к питон 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')] 
+0

Код, показанный в конце [этого ответа] (http://stackoverflow.com/a/18561055/355230) к связанному вопросу, выполняет то же самое с помощью [только] декодирования и кодирования байтового объекта 'json.dumps () 'возвращается в/из' 'latin1'', пропуская «base64», что не нужно. – martineau

Смежные вопросы