是否可以在不将编码器传递给 json.dumps() 的情况下转储 json 中的枚举?

Posted

技术标签:

【中文标题】是否可以在不将编码器传递给 json.dumps() 的情况下转储 json 中的枚举?【英文标题】:Is it possible to dump an enum in json without passing an encoder to json.dumps()? 【发布时间】:2016-08-10 12:25:19 【问题描述】:

我的问题可以用下面的例子来概括:

from enum import Enum
import json

class FooBarType(Enum):
    standard = 0
    foo = 1
    bar = 2

dict = 'name': 'test', 'value': 'test', 'type': FooBarType.foo

json.dumps(dict)

TypeError: <FooBarType.foo: 1> is not JSON serializable

我收到类型错误,因为枚举不是 JSON 可序列化的。

我主要是实现JsonEncoder 并将其添加到json.dumps() 调用,但我无法更改进行json.dumps() 调用的行。

所以,我的问题是: 是否可以在不将编码器传递给json.dumps() 的情况下转储json 中的枚举,而是通过在FooBarType 枚举中添加类方法?

我希望提取以下 json:

'name': 'test', 'value': 'test', 'type': 'foo'

'name': 'test', 'value': 'test', 'type': 1

【问题讨论】:

即使您无法更改进行json.dumps() 调用的行,您也可以在json 模块中对dumps() 函数进行monkeypatch importing 它 - 然后这将影响所有从那时起使用它(在脚本运行中)。 this answer中有打补丁的例子, 你想输出什么?是枚举“名称”FooBarType.foo 还是实际值“1”? @NilsZiehn 我更喜欢获取枚举名称,但如果不可能,相应的值就足够了。 【参考方案1】:

试试:

from enum import Enum

# class StrEnum(str, Enum):
#     """Enum where members are also (and must be) strs"""

class Color(str, Enum):
    RED = 'red'
    GREEN = 'green'
    BLUE = 'blue'


data = [
    
        'name': 'car',
        'color': Color.RED,
    ,
    
        'name': 'dog',
        'color': Color.BLUE,
    ,
]

import json
print(json.dumps(data))

结果:

[
    
        "name": "car",
        "color": "red"
    ,
    
        "name": "dog",
        "color": "blue"
    
]

【讨论】:

非常好!谢谢 这应该是答案! 这种方法可能会产生意想不到的副作用;参见例如***.com/q/65339635/10009545【参考方案2】:

遗憾的是,在 JSON 中没有对 Enum 的直接支持。

最接近的自动支持是使用IntEnumenum34 也支持),然后json 会将您的enums 视为ints;当然,解码它们会给你一个int 回复,但不指定编码器/解码器也一样好。

【讨论】:

【参考方案3】:

仅将方法添加到 FooBarType 枚举不会满足您的要求。

正如我在评论中提到的那样,您可以使用我对问题 Making object JSON serializable with regular encoder 的部分回答来猴子修补 json 模块,以便它返回 Enum 成员的名称(或值)。我假设您正在使用 Ethan Furman 等人的 enums34 模块,该模块被反向移植到 Python 2.7,因为该版本没有内置它——它成为了 standard library in Python 3.4 的一部分。

请注意,即使您无法更改发生json.dumps() 调用的行,只要在应用补丁之后发生这种情况,这也将起作用。这是因为 Python 通常会在 sys.modules 中缓存 imported 模块,即它们不会在每次在单独的脚本中使用时都重新加载——因此对它们所做的任何更改都是“粘性的”并且仍然有效。

所以对于你想做的事情,首先创建你自己的模块来制作补丁。例如:make_enum_json_serializable.py

""" Module that monkey-patches the json module when it's imported so
JSONEncoder.default() automatically checks to see if the object being encoded
is an instance of an Enum type and, if so, returns its name.
"""
from enum import Enum
from json import JSONEncoder

_saved_default = JSONEncoder().default  # Save default method.

def _new_default(self, obj):
    if isinstance(obj, Enum):
        return obj.name  # Could also be obj.value
    else:
        return _saved_default

JSONEncoder.default = _new_default # Set new default method.

然后,在您自己的脚本中,您只需添加一行即可:

from enum import Enum
import json
import make_enum_json_serializable  # ADDED

class FooBarType(Enum):
    standard = 0
    foo = 1
    bar = 2

a_dict = 'name': 'spam', 'value': 42, 'type': FooBarType.foo

print(json.dumps(a_dict))

输出:

"type": "foo", "name": "spam", "value": 42

【讨论】:

不错。您可以包含用于从 json 中获取枚举的 sn-p 吗? @Ethan:就目前而言,我认为没有任何方法可以做到这一点,因为信息丢失——来自Enum 的值没有被保留。副手我不知道怎么可能,因为 JSON 标准只允许值是字符串、数字、对象、数组、truefalse,或 null. @Ethan:my answer 到问题Making object JSON serializable with regular encoder 的第二部分描述了一种将pickle 数据存储为JSON 值的方法,这意味着可以重构实际的实例值及其类型。请注意,它确实需要在任何 json.loads() 调用上提供自定义 object_hook= 函数参数。 谢谢。我一直在寻找一种简单的方法来制作可用于将数据传输到其他系统以及返回 Python 的 json 文件,但我没有看到它。 如果走那条路,我会使用"EnumName.Member" 使解析更容易。 (恭喜你获得徽章。;)【参考方案4】:

更新:请阅读@gil9red的答案,我觉得比我的好!

我认为这没有什么好方法,您将失去 Enum 的功能。

最简单的选择:不要子类化枚举:

class FooBarType:
    standard = 0
    foo = 1
    bar = 2

dict = 'type': FooBarType.foo
json.dumps(dict)

你还可以做什么:

class EnumIntValue(int):
    def __new__(cls, name, value):
        c = int.__new__(cls, int(value))
        c.name = name
        return c
    def __repr__(self):
        return self.name
    def __str__(self):
        return self.name

class FooBarType:
    standard = EnumIntValue('standard',0)
    foo = EnumIntValue('foo',0)
    bar = EnumIntValue('bar',2)

dict = 'type': FooBarType.foo
json.dumps(dict)

这实际上会给你

"type": foo

因此不是真正有效的 json,但您可以使用它来满足您的需求!

【讨论】:

谢谢 Nils,你的第二个建议似乎是最好的答案;)【参考方案5】:

我最近遇到了一种情况,我必须序列化一个具有几个 Enum 类型作为成员的对象。

基本上,我刚刚添加了一个帮助函数,将枚举类型映射到它们的名称。

from enum import Enum, auto
from json import dumps

class Status(Enum):
    OK = auto()
    NOT_OK = auto()


class MyObject:
    def __init__(self, status):
        self.status = status


obja = MyObject(Status.OK)
objb = MyObject(Status.NOT_OK)

print(dumps(obja))
print(dumps(objb))

这当然会失败并出现错误TypeError: Object of type MyObject is not JSON serializable,因为MyObject 实例的status 成员不可序列化。

from enum import Enum, auto
from json import dumps


def _prepare_for_serialization(obj):
    serialized_dict = dict()
    for k, v in obj.__dict__.items():
        serialized_dict[k] = v.name if isinstance(v, Enum) else v
    return serialized_dict


class Status(Enum):
    OK = auto()
    NOT_OK = auto()


class MyObject:
    def __init__(self, status):
        self.status = status


obja = MyObject(Status.OK)
objb = MyObject(Status.NOT_OK)

print(dumps(_prepare_for_serialization(obja)))
print(dumps(_prepare_for_serialization(objb)))

打印出来:

"status": "OK"
"status": "NOT_OK"

后来,我使用了相同的辅助函数来为序列化的字典挑选键。

【讨论】:

【参考方案6】:

您可以使用元类代替枚举,也可以使用没有这些副作用的多重继承。

https://gist.github.com/earonesty/81e6c29fa4c54e9b67d9979ddbd8489d

例如:

class FooBarType(metaclass=TypedEnum):
    standard = 0
    foo = 1
    bar = 2

这样每个实例都是一个整数,也是一个 FooBarType。

下面的元类。

class TypedEnum(type):
    """This metaclass creates an enumeration that preserves isinstance(element, type)."""

    def __new__(mcs, cls, bases, classdict):
        """Discover the enum members by removing all intrinsics and specials."""
        object_attrs = set(dir(type(cls, (object,), )))
        member_names = set(classdict.keys()) - object_attrs
        member_names = member_names - set(name for name in member_names if name.startswith("_") and name.endswith("_"))
        new_class = None
        base = None
        for attr in member_names:
            value = classdict[attr]
            if new_class is None:
                # base class for all members is the type of the value
                base = type(classdict[attr])
                ext_bases = (*bases, base)
                new_class = super().__new__(mcs, cls, ext_bases, classdict)
                setattr(new_class, "__member_names__", member_names)
            else:
                if not base == type(classdict[attr]):  # noqa
                    raise SyntaxError("Cannot mix types in TypedEnum")
            new_val = new_class.__new__(new_class, value)
            setattr(new_class, attr, new_val)

        for parent in bases:
            new_names = getattr(parent, "__member_names__", set())
            member_names |= new_names
            for attr in new_names:
                value = getattr(parent, attr)
                if not isinstance(value, base):
                    raise SyntaxError("Cannot mix inherited types in TypedEnum: %s from %s" % (attr, parent))
                # convert all inherited values to the new class
                setattr(new_class, attr, new_class(value))

        return new_class

    def __call__(cls, arg):
        for name in cls.__member_names__:
            if arg == getattr(cls, name):
                return type.__call__(cls, arg)
        raise ValueError("Invalid value '%s' for %s" % (arg, cls.__name__))

    @property
    def __members__(cls):
        """Sufficient to make the @unique decorator work."""

        class FakeEnum:  # pylint: disable=too-few-public-methods
            """Object that looks a bit like an Enum instance."""

            def __init__(self, name, value):
                self.name = name
                self.value = value

        return name: FakeEnum(name, getattr(cls, name)) for name in cls.__member_names__

    def __iter__(cls):
        """List all enum values."""
        return (getattr(cls, name) for name in cls.__member_names__)

    def __len__(cls):
        """Get number of enum values."""
        return len(cls.__member_names__)

【讨论】:

以上是关于是否可以在不将编码器传递给 json.dumps() 的情况下转储 json 中的枚举?的主要内容,如果未能解决你的问题,请参考以下文章

如何在不将道具传递给底层 DOM 元素的情况下扩展样式组件?

如何在不将图像保存在本地的情况下将捕获的图像(Surface View)传递给另一个片段?

json.dumps参数之解

使用常规编码器使对象 JSON 可序列化

python 解析json loads dumps

将用户构建的 json 编码器传递到 Flask 的 jsonify