繁体   English   中英

如何 JSON 序列化集合?

[英]How to JSON serialize sets?

我有一个 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返回的值是按键,还是只是查看整个对象的通用包含/丢弃? 该方法如何适应嵌套值? 我已经查看了以前的问题,似乎找不到针对特定情况进行编码的最佳方法(不幸的是,这似乎是我需要在这里做的)。

您可以创建一个自定义编码器,在遇到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表示法只有少数原生数据类型(对象、数组、字符串、数字、布尔值和 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):
        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 模块)可能会很有用。 这些都支持更大范围的数据类型。

您不需要制作自定义编码器类来提供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]

我将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上找到它。

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

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

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

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

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

@AnttiHaapala 的缩短版:

json.dumps(dict_with_sets, default=lambda x: list(x) if isinstance(x, set) else x)
>>> import json
>>> set_object = set([1,2,3,4])
>>> json.dumps(list(set_object))
'[1, 2, 3, 4]'

公认解决方案的一个缺点是它的输出非常特定于 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键。 显然可以随意使用另一个更深层次的混淆密钥。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM