【问题标题】:Encoding Python Enum to JSON将 Python 枚举编码为 JSON
【发布时间】:2017-10-06 20:25:16
【问题描述】:

我有一个字典,其中一些键是 Enum 实例(enum.Enum 的子类)。我正在尝试根据documentation 使用自定义 JSON 编码器类将字典编码为 JSON 字符串。我想要的只是让输出的 JSON 中的键成为枚举名称的字符串。例如{ TestEnum.one : somevalue } 将被编码为{ "one" : somevalue }

我已经编写了一个简单的测试用例,如下所示,我在一个干净的 virtualenv 中进行了测试:

import json

from enum import Enum

class TestEnum(Enum):
    one = "first"
    two = "second"
    three = "third"

class TestEncoder(json.JSONEncoder):
    """ Custom encoder class """

    def default(self, obj):

        print("Default method called!")

        if isinstance(obj, TestEnum):
            print("Seen TestEnum!")
            return obj.name

        return json.JSONEncoder.default(self, obj)

def encode_enum(obj):
    """ Custom encoder method """

    if isinstance(obj, TestEnum):
        return obj.name
    else:
        raise TypeError("Don't know how to decode this")

if __name__ == "__main__":

    test = {TestEnum.one : "This",
            TestEnum.two : "should",
            TestEnum.three : "work!"}

    # Test dumps with Encoder method
    #print("Test with encoder method:")
    #result = json.dumps(test, default=encode_enum)
    #print(result)

    # Test dumps with Encoder Class
    print("Test with encoder class:")
    result = json.dumps(test, cls=TestEncoder)
    print(result)

我无法成功地对字典进行编码(使用 Python 3.6.1)。我不断收到TypeError: keys must be a string 错误,并且我的自定义编码器实例的默认方法(通过json.dumps 方法的cls 参数提供)似乎从未被调用?我还尝试通过json.dumps 方法的default 参数提供自定义编码方法,但同样从未触发。

我见过涉及 IntEnum 类的解决方案,但我需要 Enum 的值是字符串。我还看到this answer 讨论了与从另一个类继承的 Enum 相关的问题。但是,我的枚举仅继承自基本 enum.Enum 类并正确响应 isinstance 调用?

当提供给json.dumps 方法时,自定义类和方法都会生成TypeError。典型输出如下所示:

$ python3 enum_test.py

Test with encoder class
Traceback (most recent call last):
  File "enum_test.py", line 59, in <module>
    result = json.dumps(test, cls=TestEncoder)
  File "/usr/lib64/python3.6/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "/usr/lib64/python3.6/json/encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib64/python3.6/json/encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
TypeError: keys must be a string

我认为问题在于 JSONEncoder 类的 encode 方法假定它知道如何序列化 Enum 类(因为触发了 iterencode 方法中的 if 语句之一),因此从不调用自定义默认方法和结尾无法序列化枚举?

任何帮助将不胜感激!

【问题讨论】:

标签: python json enums python-3.6


【解决方案1】:

除了 strings 之外,您不能使用任何东西作为要转换为 JSON 的字典中的键。编码器没有给你任何其他选择; default 钩子只为未知类型的值调用,从不为键调用。

预先将您的键转换为字符串:

def convert_keys(obj, convert=str):
    if isinstance(obj, list):
        return [convert_keys(i, convert) for i in obj]
    if not isinstance(obj, dict):
        return obj
    return {convert(k): convert_keys(v, convert) for k, v in obj.items()}

json.dumps(convert_keys(test))

这会递归处理您的字典键。请注意,我包括了一个钩子;然后您可以选择如何将枚举值转换为字符串:

def enum_names(key):
    if isinstance(key, TestEnum):
        return key.name
    return str(key)

json.dumps(convert_keys(test, enum_names))

从 JSON 加载时,您可以使用相同的函数来反转该过程:

def names_to_enum(key):
    try:
        return TestEnum[key]
    except KeyError:
        return key

convert_keys(json.loads(json_data), names_to_enum)

演示:

>>> def enum_names(key):
...     if isinstance(key, TestEnum):
...         return key.name
...     return str(key)
...
>>> json_data = json.dumps(convert_keys(test, enum_names))
>>> json_data
'{"one": "This", "two": "should", "three": "work!"}'
>>> def names_to_enum(key):
...     try:
...         return TestEnum[key]
...     except KeyError:
...         return key
...
>>> convert_keys(json.loads(json_data), names_to_enum)
{<TestEnum.one: 'first'>: 'This', <TestEnum.two: 'second'>: 'should', <TestEnum.three: 'third'>: 'work!'}

【讨论】:

  • 效果很好!谢谢你。遗憾的是,没有办法通过编码器来做到这一点,而且文档对此也没有那么清楚。
  • 很好的解决方案!应该以某种方式采用到 json.Encoder 中。
【解决方案2】:

这是一个老问题。但是没有人给出这个非常简单的答案。

你只需要从 str 继承你的枚举。

import json

from enum import Enum

class TestEnum(str, Enum):
    one = "first"
    two = "second"
    three = "third"

test = {TestEnum.one : "This",
        TestEnum.two : "should",
        TestEnum.three : "work!"}

print(json.dumps(test))

输出:

{"first": "This", "second": "should", "third": "work!"}

【讨论】:

    【解决方案3】:

    我不再使用内置的 python 枚举,我使用一个名为“TypedEnum”的元类。

    原因是元类允许我的字符串枚举像字符串一样工作:它们可以传递给接受字符串的函数,它们可以序列化为字符串(就像你想要的那样......就在 JSON 编码中),但它们仍然是一个强类型(isA Enum)。

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

    我在常规枚举中遇到的奇怪错误数不胜数。

    class TypedEnum(type):
        """This metaclass creates an enumeration that preserve 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])
                    new_class = super().__new__(mcs, cls, (base, ), classdict)
                    setattr(new_class, "__member_names__", member_names)
                else:
                    if not base == type(classdict[attr]):           # noqa
                        raise SyntaxError("Cannot mix types in TypedEnum")
                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__))
    
        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__)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-06-24
      • 2021-09-11
      • 2022-01-17
      • 2020-10-07
      • 1970-01-01
      • 2019-01-06
      • 2014-12-13
      相关资源
      最近更新 更多