如何 JSON 序列化集合?
Posted
技术标签:
【中文标题】如何 JSON 序列化集合?【英文标题】:How to JSON serialize sets? 【发布时间】:2012-01-04 01:03:34 【问题描述】:我有一个 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
我知道我可以创建具有自定义 default
方法的 json.JSONEncoder
类的扩展,但我什至不确定从哪里开始转换 set
。我是否应该在默认方法中从 set
值中创建一个字典,然后返回其编码?理想情况下,我想让默认方法能够处理原始编码器阻塞的所有数据类型(我使用 Mongo 作为数据源,因此日期似乎也会引发此错误)
任何正确方向的提示将不胜感激。
编辑:
感谢您的回答!也许我应该更准确。
我利用(并赞成)这里的答案来解决正在翻译的 set
的限制,但也有内部密钥是一个问题。
set
中的对象是转换为__dict__
的复杂对象,但它们本身也可以包含其属性值,这些值可能不适用于 json 编码器中的基本类型。
set
有很多不同的类型,哈希基本上计算实体的唯一 id,但在 NoSQL 的真正精神中,并不能准确地说明子对象包含什么。
一个对象可能包含starts
的日期值,而另一个对象可能有一些其他模式,其中不包含包含“非原始”对象的键。
这就是为什么我能想到的唯一解决方案是扩展 JSONEncoder
以替换 default
方法以打开不同的情况 - 但我不确定如何解决这个问题,而且文档不明确。在嵌套对象中,从default
返回的值是按键,还是只是查看整个对象的通用包含/丢弃?该方法如何适应嵌套值?我查看了以前的问题,似乎找不到针对特定情况进行编码的最佳方法(不幸的是,这似乎是我在这里需要做的)。
【问题讨论】:
为什么是dict
s?我认为您只想从集合中制作一个list
,然后将其传递给编码器...例如:encode(list(myset))
您可以使用 YAML(JSON 本质上是 YAML 的子集)来代替 JSON。
@PaoloMoretti:它有什么优势吗?我不认为集合是 YAML 普遍支持的数据类型之一,而且它的支持范围不太广,尤其是在 API 方面。
@PaoloMoretti 感谢您的输入,但应用程序前端需要 JSON 作为返回类型,并且此要求已用于所有目的。
@delnan 我建议使用 YAML,因为它原生支持 sets 和 dates。
【参考方案1】:
您可以创建一个自定义编码器,在遇到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)
...
>>> 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"]'
【讨论】:
谢谢,我编辑了问题以更好地说明这是我需要的类型。我似乎无法理解的是这种方法将如何处理嵌套对象。在您的示例中,返回值是集合的列表,但是如果传入的对象是包含日期(另一种错误数据类型)的集合怎么办?我应该在默认方法本身中钻取键吗?非常感谢! 我认为 JSON 模块会为您处理嵌套对象。一旦它取回列表,它将遍历列表项以尝试对每个项进行编码。如果其中一个是日期,default
函数将再次被调用,这次 obj
是一个日期对象,所以你只需要测试它并返回一个日期表示。
因此,可以想象默认方法可以为传递给它的任何一个对象运行多次,因为一旦“列出”它也会查看各个键?
有点,对于 same 对象,它不会被多次调用,但它可以递归到子对象中。查看更新的答案。
@jterrace 在 json.loads 时有什么想法可以恢复这个(要设置的列表)吗?喜欢在SetEncoder
期间对这些信息进行编码吗?【参考方案2】:
JSON 表示法只有少数原生数据类型(对象、数组、字符串、数字、布尔值和 null),因此任何用 JSON 序列化的内容都需要表示为这些类型之一。
如json module docs 所示,这种转换可以由 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 module)可能会很有用。这些都支持更大范围的数据类型。
【讨论】:
这是我第一次听说 YAML 比 JSON 更通用...o_O @KarlKnechtel YAML 是 JSON 的超集(非常接近)。它还为二进制数据、集合、有序映射和时间戳添加标签。支持更多数据类型是我所说的“更通用”的意思。您似乎在不同意义上使用“通用”一词。 不要忘记jsonpickle,它旨在成为一个通用库,用于将 Python 对象酸洗为 JSON,就像这个答案所暗示的那样。 从 1.2 版开始,YAML 是 JSON 的严格超集。现在所有合法的 JSON 都是合法的 YAML。 yaml.org/spec/1.2/spec.html 此代码示例导入JSONDecoder
但未使用它【参考方案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]
。
【讨论】:
最简单、易读和优雅的解决方案。我个人更喜欢 dict 而不是列表,因为 dict 实际上是一个集合(有好处)。 @BerryTsakala 但 json 对象不能将整数作为键...【参考方案4】:我将 Raymond Hettinger's solution 改编为 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')]
【讨论】:
this answer 结尾处显示的相关问题的代码通过[仅] 解码和编码字节对象json.dumps()
返回/来自'latin1'
,跳过@ 987654333@ 不必要的东西。【参考方案5】:
如果您只需要快速转储并且不想实现自定义编码器。您可以使用以下内容:
json_string = json.dumps(data, iterable_as_array=True)
这会将所有集合(和其他可迭代对象)转换为数组。请注意,当您解析 JSON 时,这些字段将保留为数组。如果要保留类型,则需要编写自定义编码器。
还要确保已安装并需要 simplejson
。
你可以在PyPi找到它。
【讨论】:
当我尝试这个时,我得到:TypeError: __init__() got an unexpected keyword argument 'iterable_as_array' 需要安装simplejson import simplejson as json 然后 json_string = json.dumps(data, iterable_as_array=True) 在 Python 3.6 中运行良好 这是唯一对我有用的答案,但它肯定需要 simplejson。【参考方案6】:只有字典、列表和原始对象类型(int、string、bool)在 JSON 中可用。
【讨论】:
“原始对象类型”在谈论 Python 时毫无意义。 “内置对象”更有意义,但在这里过于宽泛(对于初学者来说:它包括字典、列表和集合)。 (不过,JSON 术语可能会有所不同。) 字符串数字对象数组 true false null【参考方案7】:如果您只需要编码集合,而不是一般的 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
【讨论】:
【参考方案8】:@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)].【参考方案9】:如果您确定唯一的不可序列化数据将是set
s,那么有一个非常简单(但很脏)的解决方案:
json.dumps("Hello World": 1, 2, default=tuple)
只有不可序列化的数据会被default
函数处理,所以只有set
会被转换成tuple
。
【讨论】:
json.dumps("Hello World": 1, 2, default=list)
也可以【参考方案10】:
accepted solution 的一个缺点是它的输出非常特定于 python。 IE。它的原始 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]'
【讨论】:
这不会保留对象的类型,而是将其变成一个列表。以上是关于如何 JSON 序列化集合?的主要内容,如果未能解决你的问题,请参考以下文章
反序列化 JSON 对象的集合元素是 LinkedHashMap
使用 .NET Newtonsoft.Json 组件反序列化某些有效 json 时,为啥我的 POCO 中的所有集合都为空