简体   繁体   English

如何 JSON 序列化集合?

[英]How to JSON serialize sets?

I have a Python set that contains objects with __hash__ and __eq__ methods in order to make certain no duplicates are included in the collection.我有一个 Python set ,其中包含具有__hash____eq__方法的对象,以确保集合中不包含重复项。

I need to json encode this result set , but passing even an empty set to the json.dumps method raises a TypeError .我需要对这个结果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

I know I can create an extension to the json.JSONEncoder class that has a custom default method, but I'm not even sure where to begin in converting over the set .我知道我可以为具有自定义default方法的json.JSONEncoder类创建一个扩展,但我什至不确定从哪里开始转换set Should I create a dictionary out of the set values within the default method, and then return the encoding on that?我应该从默认方法中的set值中创建一个字典,然后返回其编码吗? Ideally, I'd like to make the default method able to handle all the datatypes that the original encoder chokes on (I'm using Mongo as a data source so dates seem to raise this error too)理想情况下,我想让默认方法能够处理原始编码器阻塞的所有数据类型(我使用 Mongo 作为数据源,因此日期似乎也会引发此错误)

Any hint in the right direction would be appreciated.任何正确方向的提示将不胜感激。

EDIT:编辑:

Thanks for the answer!感谢你的回答! Perhaps I should have been more precise.也许我应该更准确。

I utilized (and upvoted) the answers here to get around the limitations of the set being translated, but there are internal keys that are an issue as well.我利用(并赞成)这里的答案来绕过正在翻译的set的限制,但也有内部键是一个问题。

The objects in the set are complex objects that translate to __dict__ , but they themselves can also contain values for their properties that could be ineligible for the basic types in the json encoder. set中的对象是转换为__dict__的复杂对象,但它们本身也可以包含其属性的值,这些值可能不适用于 json 编码器中的基本类型。

There's a lot of different types coming into this set , and the hash basically calculates a unique id for the entity, but in the true spirit of NoSQL there's no telling exactly what the child object contains.这个set中有很多不同的类型,散列基本上为实体计算一个唯一的 id,但在 NoSQL 的真正精神中,并不能准确地说明子对象包含什么。

One object might contain a date value for starts , whereas another may have some other schema that includes no keys containing "non-primitive" objects.一个对象可能包含starts的日期值,而另一个对象可能有一些其他模式,其中不包含包含“非原始”对象的键。

That is why the only solution I could think of was to extend the JSONEncoder to replace the default method to turn on different cases - but I'm not sure how to go about this and the documentation is ambiguous.这就是为什么我能想到的唯一解决方案是扩展JSONEncoder以替换default方法以打开不同的情况 - 但我不确定如何解决这个问题,并且文档不明确。 In nested objects, does the value returned from default go by key, or is it just a generic include/discard that looks at the whole object?在嵌套对象中, default返回的值是按键,还是只是查看整个对象的通用包含/丢弃? How does that method accommodate nested values?该方法如何适应嵌套值? I've looked through previous questions and can't seem to find the best approach to case-specific encoding (which unfortunately seems like what I'm going to need to do here).我已经查看了以前的问题,似乎找不到针对特定情况进行编码的最佳方法(不幸的是,这似乎是我需要在这里做的)。

You can create a custom encoder that returns a list when it encounters a set .您可以创建一个自定义编码器,在遇到set时返回一个list Here's an example:这是一个例子:

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

You can detect other types this way too.您也可以通过这种方式检测其他类型。 If you need to retain that the list was actually a set, you could use a custom encoding.如果您需要保留该列表实际上是一个集合,则可以使用自定义编码。 Something like return {'type':'set', 'list':list(obj)} might work.return {'type':'set', 'list':list(obj)}这样的东西可能会起作用。

To illustrated nested types, consider serializing this:为了说明嵌套类型,请考虑将其序列化:

>>> class Something(object):
...    pass
>>> json.dumps(set([1,2,3,4,5,Something()]), cls=SetEncoder)

This raises the following error:这会引发以下错误:

TypeError: <__main__.Something object at 0x1691c50> is not JSON serializable

This indicates that the encoder will take the list result returned and recursively call the serializer on its children.这表明编码器将获取返回的list结果并递归调用其子级的序列化程序。 To add a custom serializer for multiple types, you can do this:要为多种类型添加自定义序列化程序,您可以执行以下操作:

>>> 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 notation has only a handful of native datatypes (objects, arrays, strings, numbers, booleans, and null), so anything serialized in JSON needs to be expressed as one of these types. JSON表示法只有少数原生数据类型(对象、数组、字符串、数字、布尔值和 null),因此在 JSON 中序列化的任何内容都需要表示为这些类型之一。

As shown in the json module docs , this conversion can be done automatically by a JSONEncoder and JSONDecoder , but then you would be giving up some other structure you might need (if you convert sets to a list, then you lose the ability to recover regular lists; if you convert sets to a dictionary using dict.fromkeys(s) then you lose the ability to recover dictionaries).json 模块 docs所示,这种转换可以由JSONEncoderJSONDecoder自动完成,但是您将放弃一些您可能需要的其他结构(如果您将集合转换为列表,那么您将失去恢复常规的能力列表;如果您使用dict.fromkeys(s)将集合转换为字典,那么您将失去恢复字典的能力)。

A more sophisticated solution is to build-out a custom type that can coexist with other native JSON types.一个更复杂的解决方案是构建一个可以与其他原生 JSON 类型共存的自定义类型。 This lets you store nested structures that include lists, sets, dicts, decimals, datetime objects, etc.:这使您可以存储嵌套结构,包括列表、集合、字典、小数、日期时间对象等:

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

Here is a sample session showing that it can handle lists, dicts, and sets:这是一个示例会话,显示它可以处理列表、字典和集合:

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

Alternatively, it may be useful to use a more general purpose serialization technique such as YAML , Twisted Jelly , or Python's pickle module .或者,使用更通用的序列化技术(例如YAMLTwisted Jelly或 Python 的pickle 模块)可能会很有用。 These each support a much greater range of datatypes.这些都支持更大范围的数据类型。

You don't need to make a custom encoder class to supply the default method - it can be passed in as a keyword argument:您不需要制作自定义编码器类来提供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)

results in [1, 2, 3] in all supported Python versions.在所有受支持的 Python 版本中产生[1, 2, 3]

I adapted Raymond Hettinger's solution to python 3.我将Raymond Hettinger 的解决方案改编为 python 3。

Here is what has changed:以下是发生的变化:

  • unicode disappeared unicode消失了
  • updated the call to the parents' default with super()使用super()更新了对父母default的调用
  • using base64 to serialize the bytes type into str (because it seems that bytes in python 3 can't be converted to JSON)使用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')]

If you need just quick dump and don't want to implement custom encoder.如果您只需要快速转储并且不想实现自定义编码器。 You can use the following:您可以使用以下内容:

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

This will convert all sets (and other iterables) into arrays.这会将所有集合(和其他可迭代对象)转换为数组。 Just beware that those fields will stay arrays when you parse the JSON back.请注意,当您解析 JSON 时,这些字段将保留为数组。 If you want to preserve the types, you need to write custom encoder.如果要保留类型,则需要编写自定义编码器。

Also make sure to have simplejson installed and required.还要确保安装并需要simplejson
You can find it on PyPi .你可以在PyPi上找到它。

If you know for sure that the only non-serializable data will be set s, there's a very simple (and dirty) solution:如果您确定唯一的不可序列化数据将是set s,那么有一个非常简单(而且很脏)的解决方案:

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

Only non-serializable data will be treated with the function given as default , so only the set will be converted to a tuple .只有不可序列化的数据会被default的函数处理,所以只有set会被转换为tuple

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

If you only need to encode sets, not general Python objects, and want to keep it easily human-readable, a simplified version of Raymond Hettinger's answer can be used:如果您只需要编码集合,而不是一般的 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]'

One shortcoming of the accepted solution is that its output is very python specific.公认解决方案的一个缺点是它的输出非常特定于 python。 Ie its raw json output cannot be observed by a human or loaded by another language (eg javascript).即它的原始 json 输出不能被人类观察或被另一种语言(例如 javascript)加载。 example:例子:

db = {
        "a": [ 44, set((4,5,6)) ],
        "b": [ 55, set((4,3,2)) ]
        }

j = dumps(db, cls=PythonObjectEncoder)
print(j)

Will get you:会给你:

{"a": [44, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsESwVLBmWFcQJScQMu"}], "b": [55, {"_python_object": "gANjYnVpbHRpbnMKc2V0CnEAXXEBKEsCSwNLBGWFcQJScQMu"}]}

I can propose a solution which downgrades the set to a dict containing a list on the way out, and back to a set when loaded into python using the same encoder, therefore preserving observability and language agnosticism:我可以提出一个解决方案,将集合降级为包含列表的字典,并在使用相同的编码器加载到 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"])

Which gets you:这让你:

{"a": [44, {"__set__": [4, 5, 6]}], "b": [55, {"__set__": [2, 3, 4]}]}
[44, {'__set__': [4, 5, 6]}]

Note that serializing a dictionary which has an element with a key "__set__" will break this mechanism.请注意,序列化具有键"__set__"的元素的字典将破坏此机制。 So __set__ has now become a reserved dict key.所以__set__现在已成为保留的dict键。 Obviously feel free to use another, more deeply obfuscated key.显然可以随意使用另一个更深层次的混淆密钥。

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

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