Кодирование Python Enum в JSON
У меня есть словарь, где некоторые ключи являются экземплярами Enum (подклассы enum.Enum). Я пытаюсь закодировать словарь в строку JSON, используя пользовательский класс JSON Encoder согласно документации. Все, что я хочу, это чтобы ключи в выводимом JSON были строками имен Enum. Например { 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
ошибки и метод по умолчанию для моего экземпляра пользовательского кодировщика (предоставляется через cls
аргумент json.dumps
метод) никогда не называется? Я также попытался предоставить собственный метод кодирования через default
аргумент json.dumps
метод, но опять же это никогда не срабатывает.
Я видел решения, включающие класс IntEnum, но мне нужно, чтобы значения Enum были строками. Я также видел этот ответ, в котором обсуждается проблема, связанная с Enum, которая наследуется от другого класса. Однако мои перечисления наследуются от базового класса enum.Enum только и правильно отвечают на isinstance
звонки?
И пользовательский класс, и метод создают TypeError
при подаче на json.dumps
метод. Типичный вывод показан ниже:
$ 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
Я предполагаю, что проблема в том, что encode
метод класса JSONEncoder предполагает, что он знает, как сериализовать класс Enum (потому что один из операторов if в iterencode
метод запущен) и поэтому никогда не вызывает пользовательских методов по умолчанию и завершается с ошибкой сериализации Enum?
Любая помощь будет принята с благодарностью!
1 ответ
Вы не можете использовать ничего, кроме строк, в качестве ключей в словарях, которые вы хотите преобразовать в JSON. Кодировщик не дает вам никаких других опций; default
hook вызывается только для значений неизвестного типа, но не для ключей.
Конвертируйте ваши ключи в строки:
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!'}
Это старый вопрос. Но никто не дал такого простого ответа.
Вам просто нужно создать подкласс Enum от 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!"}
Я больше не использую встроенное перечисление 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__)