如何JSON序列化集?

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

我需要json编码这个结果set,但即使传递一个空的setjson.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

我知道我可以为具有自定义default方法的json.JSONEncoder类创建一个扩展,但我甚至不确定从哪里开始转换set。我是否应该在默认方法中创建一个set值的字典,然后返回编码?理想情况下,我希望使默认方法能够处理原始编码器阻塞的所有数据类型(我使用Mongo作为数据源,因此日期似乎也会引发这个错误)

任何正确方向的提示都将不胜感激。

编辑:

谢谢你的回答!也许我应该说得更准确些。

我利用(并投票)这里的答案来解决set被翻译的限制,但内部键也是一个问题。

set中的对象是可转换为__dict__的复杂对象,但它们本身也可以包含其属性的值,这些值可能不适用于json编码器中的基本类型。

set中有很多不同的类型,散列基本上为实体计算一个唯一的id,但在NoSQL的真正精神中,并没有确切地告诉子对象包含什么。

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

这就是为什么我能想到的唯一解决方案是扩展JSONEncoder来取代default方法来打开不同的情况-但我不确定如何做到这一点,而且文档是模糊的。在嵌套对象中,从default返回的值是按键返回的,还是只是一个通用的包含/丢弃,查看整个对象?该方法如何容纳嵌套值?我已经浏览了以前的问题,但似乎无法找到特定于情况的编码的最佳方法(不幸的是,这似乎是我在这里需要做的)。

295329 次浏览

JSON表示法只有少数几种原生数据类型(对象、数组、字符串、数字、布尔值和null),因此任何在JSON中序列化的东西都需要表示为这些类型之一。

Json模块文档所示,这种转换可以由JSONEncoderJSONDecoder自动完成,但这样你就会放弃一些你可能需要的其他结构(如果你将集合转换为列表,那么你就失去了恢复常规列表的能力;如果你使用dict.fromkeys(s)将集合转换为字典,那么你将失去恢复字典的能力)。

更复杂的解决方案是构建一个可以与其他原生JSON类型共存的自定义类型。这让你可以存储嵌套结构,包括列表,集,字典,小数,datetime对象等:

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

或者,使用更通用的序列化技术(如YAML扭曲的果冻或Python的pickle模块)可能会有用。它们各自支持更大范围的数据类型。

JSON中只有字典、列表和基本对象类型(int、string、bool)可用。

你可以创建一个自定义编码器,当它遇到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"]'

如果你只需要编码集合,而不是一般的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

我将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')]

如果你只是需要快速转储,不想实现自定义编码器。你可以使用以下方法:

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

这将把所有集合(和其他可迭代对象)转换为数组。只是要注意,当解析回JSON时,这些字段将保持数组。如果要保留类型,则需要编写自定义编码器。

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

接受的解决方案的一个缺点是它的输出非常特定于python。也就是说,它的原始json输出不能被人类观察到,也不能被其他语言(如javascript)加载。 例子:< / p >
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"}]}

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

你不需要创建一个自定义编码器类来提供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]

@AnttiHaapala的简写:

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

如果你确定唯一不可序列化的数据将是sets,有一个非常简单(和脏)的解决方案:

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

只有不可序列化的数据才会被default函数处理,所以只有set会被转换为tuple

>>> import json
>>> set_object = set([1,2,3,4])
>>> json.dumps(list(set_object))
'[1, 2, 3, 4]'

你应该试试jsonwhatever

https://pypi.org/project/jsonwhatever/

PIP安装jsonwhatever

from jsonwhatever import JsonWhatEver


set_a = {1,2,3}


jsonwe = JsonWhatEver()


string_res = jsonwe.jsonwhatever('set_string', set_a)


print(string_res)