Обратите внимание, что это похоже на Как получить методы @property в asdict? .
У меня есть (замороженная) вложенная структура данных, подобная следующей. Определены несколько свойств, которые (чисто) зависят от полей.
import copy
import dataclasses
import json
from dataclasses import dataclass
@dataclass(frozen=True)
class Bar:
x: int
y: int
@property
def z(self):
return self.x + self.y
@dataclass(frozen=True)
class Foo:
a: int
b: Bar
@property
def c(self):
return self.a + self.b.x - self.b.y
Я могу сериализовать структуру данных следующим образом:
class CustomEncoder(json.JSONEncoder):
def default(self, o):
if dataclasses and dataclasses.is_dataclass(o):
return dataclasses.asdict(o)
return json.JSONEncoder.default(self, o)
foo = Foo(1, Bar(2,3))
print(json.dumps(foo, cls=CustomEncoder))
# Outputs {"a": 1, "b": {"x": 2, "y": 3}}
Однако я хотел бы также сериализовать свойства (@property
). Обратите внимание: я не хочу превращать свойства в поля с помощью __post_init__
, так как я хотел бы, чтобы класс данных оставался замороженным. Я не хочу использовать obj.__setattr__
для обхода замороженных полей. Я также не хочу предварительно вычислять значения свойства вне класса и передать их как поля.
Текущее решение, которое я использую, заключается в том, чтобы явно указать, как каждый объект сериализуется, следующим образом:
class CustomEncoder2(json.JSONEncoder):
def default(self, o):
if isinstance(o, Foo):
return {
"a": o.a,
"b": o.b,
"c": o.c
}
elif isinstance(o, Bar):
return {
"x": o.x,
"y": o.y,
"z": o.z
}
return json.JSONEncoder.default(self, o)
foo = Foo(1, Bar(2,3))
print(json.dumps(foo, cls=CustomEncoder2))
# Outputs {"a": 1, "b": {"x": 2, "y": 3, "z": 5}, "c": 0} as desired
Для нескольких уровней вложенности это выполнимо, но я надеюсь на более общее решение. Например, вот (хакерское) решение, которое обезьяно исправляет реализацию _asdict_inner из библиотеки классов данных.
def custom_asdict_inner(obj, dict_factory):
if dataclasses._is_dataclass_instance(obj):
result = []
for f in dataclasses.fields(obj):
value = custom_asdict_inner(getattr(obj, f.name), dict_factory)
result.append((f.name, value))
# Inject this one-line change
result += [(prop, custom_asdict_inner(getattr(obj, prop), dict_factory)) for prop in dir(obj) if not prop.startswith('__')]
return dict_factory(result)
elif isinstance(obj, tuple) and hasattr(obj, '_fields'):
return type(obj)(*[custom_asdict_inner(v, dict_factory) for v in obj])
elif isinstance(obj, (list, tuple)):
return type(obj)(custom_asdict_inner(v, dict_factory) for v in obj)
elif isinstance(obj, dict):
return type(obj)((custom_asdict_inner(k, dict_factory),
custom_asdict_inner(v, dict_factory))
for k, v in obj.items())
else:
return copy.deepcopy(obj)
dataclasses._asdict_inner = custom_asdict_inner
class CustomEncoder3(json.JSONEncoder):
def default(self, o):
if dataclasses and dataclasses.is_dataclass(o):
return dataclasses.asdict(o)
return json.JSONEncoder.default(self, o)
foo = Foo(1, Bar(2,3))
print(json.dumps(foo, cls=CustomEncoder3))
# Outputs {"a": 1, "b": {"x": 2, "y": 3, "z": 5}, "c": 0} as desired
Есть ли рекомендуемый способ добиться того, что я пытаюсь сделать?