在 asdict 或序列化中将属性包含在数据类中的推荐方法是什么?
What is the recommended way to include properties in dataclasses in asdict or serialization?
注意这类似于 。
我有一个(冻结的)嵌套数据结构,如下所示。定义了一些(完全)依赖于字段的属性。
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__
将属性转换为字段,因为我想保持数据 class' 冻结。 I do not want to use obj.__setattr__
to work around the frozen fields. 我也不想预先计算 class 之外的属性值并将它们作为字段传递。
我目前使用的解决方案是明确写出每个对象是如何序列化的,如下所示:
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
对于几级嵌套,这是可以管理的,但我希望有一个更通用的解决方案。例如,这是一个(hacky)解决方案,它从 dataclasses 库中猴子修补 _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
是否有推荐的方法来实现我想要做的事情?
这似乎与方便的 dataclass
功能相矛盾:
Class(**asdict(obj)) == obj # only for classes w/o nested dataclass attrs
如果您没有找到任何相关的 pypi 包,您可以随时添加一个 2-liner,如下所示:
from dataclasses import asdict as std_asdict
def asdict(obj):
return {**std_asdict(obj),
**{a: getattr(obj, a) for a in getattr(obj, '__add_to_dict__', [])}}
然后您可以自定义但简短的方式在字典中指定您想要的:
@dataclass
class A:
f: str
__add_to_dict__ = ['f2']
@property
def f2(self):
return self.f + '2'
@dataclass
class B:
f: str
print(asdict(A('f')))
print(asdict(B('f')))
:
{'f2': 'f2', 'f': 'f'}
{'f': 'f'}
据我所知,没有包含它们的“推荐”方法。
这里有一些似乎有用的东西,我认为可以满足您的众多要求。它定义了一个自定义编码器,当对象是 dataclass
时调用它自己的 _asdict()
方法,而不是猴子修补(私有)dataclasses._asdict_inner()
函数和 encapsulates(捆绑)使用它的客户编码器中的代码。
像您一样,我将 dataclasses.asdict()
的当前实现用作 guide/template,因为您所要求的基本上只是它的自定义版本。每个 property
字段的当前值是通过调用其 __get__
方法获得的。
import copy
import dataclasses
from dataclasses import dataclass, field
import json
import re
from typing import List
class MyCustomEncoder(json.JSONEncoder):
is_special = re.compile(r'^__[^\d\W]\w*__\Z', re.UNICODE) # Dunder name.
def default(self, obj):
return self._asdict(obj)
def _asdict(self, obj, *, dict_factory=dict):
if not dataclasses.is_dataclass(obj):
raise TypeError("_asdict() should only be called on dataclass instances")
return self._asdict_inner(obj, dict_factory)
def _asdict_inner(self, obj, dict_factory):
if dataclasses.is_dataclass(obj):
result = []
# Get values of its fields (recursively).
for f in dataclasses.fields(obj):
value = self._asdict_inner(getattr(obj, f.name), dict_factory)
result.append((f.name, value))
# Add values of non-special attributes which are properties.
is_special = self.is_special.match # Local var to speed access.
for name, attr in vars(type(obj)).items():
if not is_special(name) and isinstance(attr, property):
result.append((name, attr.__get__(obj))) # Get property's value.
return dict_factory(result)
elif isinstance(obj, tuple) and hasattr(obj, '_fields'):
return type(obj)(*[self._asdict_inner(v, dict_factory) for v in obj])
elif isinstance(obj, (list, tuple)):
return type(obj)(self._asdict_inner(v, dict_factory) for v in obj)
elif isinstance(obj, dict):
return type(obj)((self._asdict_inner(k, dict_factory),
self._asdict_inner(v, dict_factory)) for k, v in obj.items())
else:
return copy.deepcopy(obj)
if __name__ == '__main__':
@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
# Added for testing.
d: List = field(default_factory=lambda: [42]) # Field with default value.
foo = Foo(1, Bar(2,3))
print(json.dumps(foo, cls=MyCustomEncoder))
输出:
{"a": 1, "b": {"x": 2, "y": 3, "z": 5}, "d": [42], "c": 0}
注意这类似于
我有一个(冻结的)嵌套数据结构,如下所示。定义了一些(完全)依赖于字段的属性。
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__
将属性转换为字段,因为我想保持数据 class' 冻结。 I do not want to use obj.__setattr__
to work around the frozen fields. 我也不想预先计算 class 之外的属性值并将它们作为字段传递。
我目前使用的解决方案是明确写出每个对象是如何序列化的,如下所示:
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
对于几级嵌套,这是可以管理的,但我希望有一个更通用的解决方案。例如,这是一个(hacky)解决方案,它从 dataclasses 库中猴子修补 _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
是否有推荐的方法来实现我想要做的事情?
这似乎与方便的 dataclass
功能相矛盾:
Class(**asdict(obj)) == obj # only for classes w/o nested dataclass attrs
如果您没有找到任何相关的 pypi 包,您可以随时添加一个 2-liner,如下所示:
from dataclasses import asdict as std_asdict
def asdict(obj):
return {**std_asdict(obj),
**{a: getattr(obj, a) for a in getattr(obj, '__add_to_dict__', [])}}
然后您可以自定义但简短的方式在字典中指定您想要的:
@dataclass
class A:
f: str
__add_to_dict__ = ['f2']
@property
def f2(self):
return self.f + '2'
@dataclass
class B:
f: str
print(asdict(A('f')))
print(asdict(B('f')))
:
{'f2': 'f2', 'f': 'f'}
{'f': 'f'}
据我所知,没有包含它们的“推荐”方法。
这里有一些似乎有用的东西,我认为可以满足您的众多要求。它定义了一个自定义编码器,当对象是 dataclass
时调用它自己的 _asdict()
方法,而不是猴子修补(私有)dataclasses._asdict_inner()
函数和 encapsulates(捆绑)使用它的客户编码器中的代码。
像您一样,我将 dataclasses.asdict()
的当前实现用作 guide/template,因为您所要求的基本上只是它的自定义版本。每个 property
字段的当前值是通过调用其 __get__
方法获得的。
import copy
import dataclasses
from dataclasses import dataclass, field
import json
import re
from typing import List
class MyCustomEncoder(json.JSONEncoder):
is_special = re.compile(r'^__[^\d\W]\w*__\Z', re.UNICODE) # Dunder name.
def default(self, obj):
return self._asdict(obj)
def _asdict(self, obj, *, dict_factory=dict):
if not dataclasses.is_dataclass(obj):
raise TypeError("_asdict() should only be called on dataclass instances")
return self._asdict_inner(obj, dict_factory)
def _asdict_inner(self, obj, dict_factory):
if dataclasses.is_dataclass(obj):
result = []
# Get values of its fields (recursively).
for f in dataclasses.fields(obj):
value = self._asdict_inner(getattr(obj, f.name), dict_factory)
result.append((f.name, value))
# Add values of non-special attributes which are properties.
is_special = self.is_special.match # Local var to speed access.
for name, attr in vars(type(obj)).items():
if not is_special(name) and isinstance(attr, property):
result.append((name, attr.__get__(obj))) # Get property's value.
return dict_factory(result)
elif isinstance(obj, tuple) and hasattr(obj, '_fields'):
return type(obj)(*[self._asdict_inner(v, dict_factory) for v in obj])
elif isinstance(obj, (list, tuple)):
return type(obj)(self._asdict_inner(v, dict_factory) for v in obj)
elif isinstance(obj, dict):
return type(obj)((self._asdict_inner(k, dict_factory),
self._asdict_inner(v, dict_factory)) for k, v in obj.items())
else:
return copy.deepcopy(obj)
if __name__ == '__main__':
@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
# Added for testing.
d: List = field(default_factory=lambda: [42]) # Field with default value.
foo = Foo(1, Bar(2,3))
print(json.dumps(foo, cls=MyCustomEncoder))
输出:
{"a": 1, "b": {"x": 2, "y": 3, "z": 5}, "d": [42], "c": 0}