如何对集合进行JSON序列化?


148

我有一个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,并且哈希基本上为实体计算了唯一的ID,但是按照NoSQL的真正精神,没有确切说明子对象包含什么。

一个对象可能包含的日期值starts,而另一个对象可能具有一些其他模式,该模式不包含包含“非原始”对象的键。

这就是为什么我唯一能想到的解决方案是扩展JSONEncoder替换default方法以打开不同情况的方法-但我不确定如何进行此操作,并且文档不明确。在嵌套对象中,是default按键返回go 的值,还是只是查看整个对象的通用包含/丢弃?该方法如何容纳嵌套值?我已经看过先前的问题,但似乎找不到最佳的针对特定情况的编码的方法(不幸的是,这似乎是我在这里需要做的事情)。


3
为什么dict呢?我认为您只是想做一个list集合,然后将其传递给编码器...例如:encode(list(myset))
Constantinius

2
除了使用JSON,还可以使用YAML(JSON本质上是YAML的子集)。
Paolo Moretti

@PaoloMoretti:它带来什么好处吗?我不认为集合是YAML普遍支持的数据类型之一,并且它的支持程度较差,尤其是在API方面。

@PaoloMoretti感谢您的输入,但是应用程序前端要求将JSON作为返回类型,并且此要求已固定用于所有目的。
DeaconDesperado

2
@delnan我建议使用YAML,因为它同时支持集合日期
Paolo Moretti

Answers:


116

JSON表示法只有少数本机数据类型(对象,数组,字符串,数字,布尔值和null),因此以JSON序列化的任何内容都必须表示为这些类型之一。

json模块docs所示,此转换可以由JSONEncoderJSONDecoder自动完成,但随后您将放弃可能需要的其他一些结构(如果将集转换为列表,则将失去恢复常规数据的能力。列表;如果使用将集转换为字典,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')]

另外,使用更通用的序列化技术(例如YAMLTwisted Jelly或Python的pickle模块)可能很有用。它们每个都支持更大范围的数据类型。


11
这是我第一次听说YAML比JSON更通用... o_O
Karl Knechtel

13
@KarlKnechtel YAML是JSON的超集(非常接近)。它还为二进制数据,集,有序映射和时间戳添加标签。支持更多数据类型是我所说的“更通用”。您似乎在以另一种含义使用“通用”一词。
Raymond Hettinger 2012年

4
别忘了jsonpickle,它旨在成为将Python对象腌制为JSON的通用库,正如这个答案所暗示的那样。
詹森·库姆斯

4
从1.2版开始,YAML是JSON的严格超集。现在所有合法的JSON都是合法的YAML。 yaml.org/spec/1.2/spec.html
steveha 2014年

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"]'

谢谢,我编辑了问题以更好地说明这是我需要的东西。我似乎无法掌握的是此方法将如何处理嵌套对象。在您的示例中,返回值是set的列表,但是如果传入的对象是其中带有日期(另一个错误的数据类型)的set怎么办?是否应该在默认方法本身中钻取键?万分感谢!
DeaconDesperado 2011年

1
我认为JSON模块可以为您处理嵌套对象。一旦返回列表,它将遍历列表项以尝试对每个项进行编码。如果其中之一是日期,则该default函数将再次obj作为日期对象被调用,因此您只需对其进行测试并返回日期表示。
jterrace

那么可以想象,默认方法可以对传递给它的任何一个对象多次运行,因为一旦“列出”它也会查看各个键?
DeaconDesperado

某种意义上讲,同一对象不会被多次调用,但是可以递归给子对象。查看最新答案。
jterrace

完全按照您的描述工作。我仍然必须找出一些错误,但是其中大多数可能是可以重构的。非常感谢您的指导!
DeaconDesperado

7

我将Raymond Hettinger的解决方案调整为适用于python 3。

这是发生了什么变化:

  • unicode 消失了
  • 更新调用父母defaultsuper()
  • 使用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不必要的操作。
martineau '16

6

JSON中仅字典,列表和原始对象类型(int,字符串,布尔)可用。


5
在谈论Python时,“原始对象类型”毫无意义。“内置对象”更有意义,但是在这里太宽泛了(对于初学者:它包括字典,列表和集合)。(不过,JSON术语可能有所不同。)

字符串数字对象数组true false null
Joseph Le Brech

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对象,并且想要使其易于阅读,则可以使用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

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"}]}

我可以提出一种解决方案,将集合降级为包含输出列表的字典,并在使用相同的编码器加载到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键。显然,可以随意使用另一个更加模糊的密钥。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.