如何将集合序列化为 JSON?[重复]
- 2025-01-20 09:07:00
- admin 原创
- 94
问题描述:
我有一个set
包含对象__hash__
和__eq__
方法的 Python,以确保集合中不包含重复项。
我需要对这个结果进行 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 返回的值是否按键,或者它只是一个查看整个对象的通用包含/丢弃?该方法如何适应嵌套值?我查看了之前的问题,似乎找不到针对特定案例的编码的最佳方法(不幸的是,这似乎是我在这里需要做的)。
解决方案 1:
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)
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"]'
解决方案 2:
JSON符号只有少数几种原生数据类型(对象、数组、字符串、数字、布尔值和 null),因此 JSON 中序列化的任何内容都需要表示为这些类型之一。
如json 模块文档中所示,此转换可以由JSONEncoder和JSONDecoder自动完成,但这样一来,您可能需要的其他一些结构(如果将集合转换为列表,则将失去恢复常规列表的能力;如果使用将集合转换为字典,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')]
或者,使用更通用的序列化技术(如YAML、Twisted Jelly或 Python 的pickle 模块)可能会有用。它们各自支持更广泛的数据类型。
解决方案 3:
您不需要创建自定义编码器类来提供该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]
。
解决方案 4:
如果您确信唯一不可序列化的数据是set
s,那么有一个非常简单(且肮脏)的解决方案:
json.dumps({"Hello World": {1, 2}}, default=tuple)
只有不可序列化的数据才会用 给出的函数进行处理default
,因此只有set
才会转换为tuple
。
解决方案 5:
我将Raymond Hettinger 的解决方案改编为 python 3。
变化的内容如下:
unicode
消失了default
更新了与家长的通话内容super()
使用
base64
将bytes
类型序列化为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')]
解决方案 6:
如果你只需要快速转储而不想实现自定义编码器。你可以使用以下命令:
json_string = json.dumps(data, iterable_as_array=True)
这会将所有集合(和其他可迭代对象)转换为数组。请注意,当您解析 JSON 时,这些字段将保留为数组。如果您想保留类型,则需要编写自定义编码器。
还要确保已simplejson
安装并需要。你可以在PyPi
上找到它。
解决方案 7:
JSON 中仅可用字典、列表和原始对象类型(int、string、bool)。
解决方案 8:
@AnttiHaapala 的缩写版本:
json.dumps(dict_with_sets, default=lambda x: list(x) if isinstance(x, set) else x)
解决方案 9:
如果您只需要对集合进行编码,而不是对一般的 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
解决方案 10:
可接受的解决方案的一个缺点是其输出非常特定于 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
键。显然,您可以随意使用其他更深层次混淆的键。
解决方案 11:
>>> import json
>>> set_object = set([1,2,3,4])
>>> json.dumps(list(set_object))
'[1, 2, 3, 4]'
解决方案 12:
你应该尝试 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)