如何将集合(sets)序列化为 JSON?

224

我有一个Python set,其中包含具有__hash____eq__方法的对象,以确保不包含重复项。

我需要将这个结果set进行JSON编码,但是将一个空的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
为什么要用字典?我认为你想把集合转换成列表,然后将其传递给编码器...例如:encode(list(myset)) - Constantinius
2
你可以使用YAML代替JSON(JSON本质上是YAML的子集)。 - Paolo Moretti
@PaoloMoretti 感谢您的建议,但应用程序前端需要JSON作为返回类型,这个要求是固定的。 - DeaconDesperado
2
@delnan,我建议使用YAML,因为它本身支持集合日期 - Paolo Moretti
@RaymondHettinger - 我正在实现您的解决方案。很巧合的是,这些数据集的评分系统是根据您的神经网络代码作为指南构建的!也许您还记得我在推特上与您互动过 =) - DeaconDesperado
显示剩余3条评论
12个回答

180

您可以创建一个自定义编码器,当遇到set时返回一个list。以下是示例:

import json
class SetEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, set):
            return list(obj)
        return json.JSONEncoder.default(self, obj)

data_str = json.dumps(set([1,2,3,4,5]), cls=SetEncoder)
print(data_str)
# Output: '[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)
 
data_str = json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)
print(data_str)
# Output: '[1, 2, 3, 4, 5, "CustomSomethingRepresentation"]'

谢谢,我编辑了问题以更好地说明我需要的是这种类型的东西。我似乎无法理解这个方法如何处理嵌套对象。在你的示例中,集合的返回值是列表,但如果传入的对象是一个包含日期(另一种不好的数据类型)的集合,该怎么办?我应该在默认方法内部穿过键吗?非常感谢! - DeaconDesperado
1
我认为JSON模块会自动处理嵌套对象。一旦它收到列表,它将迭代列表项尝试对每个进行编码。如果其中一个是日期,则“default”函数将再次被调用,并且这次“obj”是一个日期对象,因此您只需要测试并返回日期表示即可。 - jterrace
所以默认方法可以理论上对传递给它的任何一个对象运行多次,因为它在“系统列出”之后也会查看各个键? - DeaconDesperado
1
@jterrace 有没有什么想法可以在json.loads期间恢复这个(从列表到集合)?比如在“SetEncoder”期间编码这些信息或者其他方法? - John Strood
@jterrace 我也有兴趣创建相应的 SetDecoder 类,但是我的尝试失败了,无法正确地将数组转换为集合。有什么想法吗? - Martim
显示剩余2条评论

131

JSON标记只有少量原生数据类型(对象,数组,字符串,数字,布尔和null),因此任何以JSON格式序列化的内容都需要表示为其中一个类型。

json模块文档所示,可以通过JSONEncoderJSONDecoder自动完成此转换,但这样您可能会失去一些其他可能需要的结构(如果将集合转换为列表,则无法恢复常规列表;如果使用dict.fromkeys(s)将集合转换为字典,则会失去恢复字典的能力)。

更复杂的解决方案是构建一个自定义类型,该类型可以与其他本机JSON类型共存。 这使您可以存储包括列表、集合、字典、小数、日期时间对象等嵌套结构:

from json import dumps, loads, JSONEncoder, JSONDecoder
import pickle

class PythonObjectEncoder(JSONEncoder):
    def default(self, obj):
        try:
            return {'_python_object': pickle.dumps(obj).decode('latin-1')}
        except pickle.PickleError:
            return super().default(obj)

def as_python_object(dct):
    if '_python_object' in dct:
        return pickle.loads(dct['_python_object'].encode('latin-1'))
    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']), {'key': 'value'}, Decimal('3.14')]

或者,使用更通用的序列化技术,例如YAMLTwisted Jelly或Python的pickle模块可能会更有用。它们各自支持更广泛的数据类型。


11
这是我第一次听说 YAML 比 JSON 更通用... o_O - Karl Knechtel
15
YAML是JSON的超集(几乎相同)。它还为二进制数据、集合、有序映射和时间戳添加了标签。支持更多数据类型是我所说的“更通用”的意思。你似乎在以不同的意义使用“通用”。 - Raymond Hettinger
6
不要忘记 jsonpickle,它旨在成为一个通用的库,将Python对象序列化为JSON,就像这个答案所建议的那样。 - Jason R. Coombs
5
从版本1.2开始,YAML是JSON的严格超集。现在所有合法的JSON都是合法的YAML。http://www.yaml.org/spec/1.2/spec.html - steveha
2
这个代码示例导入了 JSONDecoder 但没有使用它。 - watsonic
显示剩余12条评论

37

您不需要创建自定义编码器类来提供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)

在所有支持的Python版本中,结果为[1, 2, 3]


最简单、可读性最强和优雅的解决方案。我个人更喜欢使用字典而不是列表,因为字典实际上是一种集合(带有优点)。 - Berry Tsakala
2
@BerryTsakala 但是 JSON 对象不能使用整数作为键... - Antti Haapala -- Слава Україні

19

如果您确定唯一不可序列化的数据将是set,那么有一个非常简单(而且有点脏)的解决方案:

json.dumps({"Hello World": {1, 2}}, default=tuple)

只有不可序列化的数据才会使用作为 default 给出的函数进行处理,所以只有 set 会被转换成 tuple


8
json.dumps({"Hello World": {1, 2}}, default=list)也可以工作。 该代码使用json.dumps()函数将Python字典转换为JSON格式,并设置default参数为list,以便在遇到不支持JSON序列化的对象时,将其转换为列表类型。 - cakraww

10

我将Raymond Hettinger的解决方案改编为Python 3。

以下是修改的内容:

  • unicode已经消失
  • 使用super()更新了对父类default的调用
  • 使用base64bytes类型序列化为str(因为在Python 3中似乎无法将bytes转换为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

8
如果您只需要快速转储,而不想实现自定义编码器。您可以使用以下方法:
json_string = json.dumps(data, iterable_as_array=True)

这将把所有的sets(以及其他iterables)转换成数组。但是请注意,当您解析JSON时,这些字段将保留为数组。如果您想要保留类型,就需要编写自定义编码器。

还要确保已安装并需要simplejson
您可以在PyPi上找到它。


13
尝试时出现以下错误:TypeError: init() got an unexpected keyword argument 'iterable_as_array'。 - atm
1
您需要安装simplejson。 - JerryBringer
3
import simplejson as json,然后json_string = json.dumps(data, iterable_as_array=True)在Python 3.6中运行良好。 - fraverta
这是唯一对我有效的答案,但它肯定需要simplejson。 - jimh

6
只有字典、列表和基本对象类型(int,string,bool)才能在JSON中使用。

8
在Python中讨论“Primitive object type”是没有意义的。在这里,“内置对象”更有意义,但太过于广泛(例如:它包括字典、列表和集合)。 (JSON术语可能会有所不同。) - user395760
字符串 数字 对象 数组 真 假 空 - Joseph Le Brech

6

@AnttiHaapala的简化版本:

json.dumps(dict_with_sets, default=lambda x: list(x) if isinstance(x, set) else x)

对我来说最好的是[set1,set2,set3,set4]。我可以通过以下方式读取字符串化:[set(i) for i in json.loads(s)]。 - H.C.Chen

5

如果你只需要编码集合而不是一般的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
>>> import json
>>> set_object = set([1,2,3,4])
>>> json.dumps(list(set_object))
'[1, 2, 3, 4]'

这不会保留对象的类型,而是将其转换为列表。 - martineau

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接