在 Python 中创建嵌套数据类对象
Creating nested dataclass objects in Python
我有一个数据类对象,其中嵌套了数据类对象。但是,当我创建主对象时,嵌套对象变成了字典:
@dataclass
class One:
f_one: int
f_two: str
@dataclass
class Two:
f_three: str
f_four: One
Two(**{'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}})
Two(f_three='three', f_four={'f_one': 1, 'f_two': 'two'})
obj = {'f_three': 'three', 'f_four': One(**{'f_one': 1, 'f_two': 'two'})}
Two(**obj)
Two(f_three='three', f_four=One(f_one=1, f_two='two'))
如您所见,只有 **obj
有效。
理想情况下,我想构建我的对象以获得如下内容:
Two(f_three='three', f_four=One(f_one=1, f_two='two'))
除了在访问对象属性时手动将嵌套字典转换为相应的数据类对象之外,还有什么方法可以实现吗?
提前致谢。
这是一个与 dataclasses
模块本身一样复杂的请求,这意味着实现这种“嵌套字段”功能的最佳方法可能是定义一个新的装饰器,类似于 @dataclass
.
幸运的是,如果您不需要 __init__
方法的签名来反映字段及其默认值,例如通过调用 dataclass
呈现的 classes,这可以变得更简单:一个 class 装饰器将调用原始 dataclass
并在其生成的 __init__
方法上包装一些功能,可以使用普通的“...(*args, **kwargs):
”样式来完成功能。
换句话说,所有需要做的就是围绕生成的 __init__
方法编写一个包装器,它将检查传入“kwargs”的参数,检查是否有对应于“dataclass 字段类型”,如果是,则在调用原始 __init__
之前生成嵌套对象。也许这在英语中比在 Python:
中更难表达
from dataclasses import dataclass, is_dataclass
def nested_dataclass(*args, **kwargs):
def wrapper(cls):
cls = dataclass(cls, **kwargs)
original_init = cls.__init__
def __init__(self, *args, **kwargs):
for name, value in kwargs.items():
field_type = cls.__annotations__.get(name, None)
if is_dataclass(field_type) and isinstance(value, dict):
new_obj = field_type(**value)
kwargs[name] = new_obj
original_init(self, *args, **kwargs)
cls.__init__ = __init__
return cls
return wrapper(args[0]) if args else wrapper
请注意,除了不用担心 __init__
签名外,这
也忽略传递 init=False
- 因为无论如何它都没有意义。
(return 行中的 if
负责此工作,无论是使用命名参数调用还是直接作为装饰器,如 dataclass
本身)
并在交互式提示上:
In [85]: @dataclass
...: class A:
...: b: int = 0
...: c: str = ""
...:
In [86]: @dataclass
...: class A:
...: one: int = 0
...: two: str = ""
...:
...:
In [87]: @nested_dataclass
...: class B:
...: three: A
...: four: str
...:
In [88]: @nested_dataclass
...: class C:
...: five: B
...: six: str
...:
...:
In [89]: obj = C(five={"three":{"one": 23, "two":"narf"}, "four": "zort"}, six="fnord")
In [90]: obj.five.three.two
Out[90]: 'narf'
如果你想保留签名,我建议使用 dataclasses
模块本身的私有辅助函数,创建一个新的 __init__
.
我没有编写新的装饰器,而是想出了一个函数,在实际 dataclass
初始化后修改所有 dataclass
类型的字段。
def dicts_to_dataclasses(instance):
"""Convert all fields of type `dataclass` into an instance of the
specified data class if the current value is of type dict."""
cls = type(instance)
for f in dataclasses.fields(cls):
if not dataclasses.is_dataclass(f.type):
continue
value = getattr(instance, f.name)
if not isinstance(value, dict):
continue
new_value = f.type(**value)
setattr(instance, f.name, new_value)
该函数可以手动调用或在__post_init__
中调用。这样 @dataclass
装饰器就可以得到充分利用。
上面的示例调用 __post_init__
:
@dataclass
class One:
f_one: int
f_two: str
@dataclass
class Two:
def __post_init__(self):
dicts_to_dataclasses(self)
f_three: str
f_four: One
data = {'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}}
two = Two(**data)
# Two(f_three='three', f_four=One(f_one=1, f_two='two'))
您可以尝试 dacite
模块。此包简化了从字典创建数据 类 - 它还支持嵌套结构。
示例:
from dataclasses import dataclass
from dacite import from_dict
@dataclass
class A:
x: str
y: int
@dataclass
class B:
a: A
data = {
'a': {
'x': 'test',
'y': 1,
}
}
result = from_dict(data_class=B, data=data)
assert result == B(a=A(x='test', y=1))
要安装英安岩,只需使用 pip:
$ pip install dacite
我已经通过@jsbueno 创建了解决方案的扩充,它也接受 List[<your class/>]
形式的输入。
def nested_dataclass(*args, **kwargs):
def wrapper(cls):
cls = dataclass(cls, **kwargs)
original_init = cls.__init__
def __init__(self, *args, **kwargs):
for name, value in kwargs.items():
field_type = cls.__annotations__.get(name, None)
if isinstance(value, list):
if field_type.__origin__ == list or field_type.__origin__ == List:
sub_type = field_type.__args__[0]
if is_dataclass(sub_type):
items = []
for child in value:
if isinstance(child, dict):
items.append(sub_type(**child))
kwargs[name] = items
if is_dataclass(field_type) and isinstance(value, dict):
new_obj = field_type(**value)
kwargs[name] = new_obj
original_init(self, *args, **kwargs)
cls.__init__ = __init__
return cls
return wrapper(args[0]) if args else wrapper
from dataclasses import dataclass, asdict
from validated_dc import ValidatedDC
@dataclass
class Foo(ValidatedDC):
one: int
two: str
@dataclass
class Bar(ValidatedDC):
three: str
foo: Foo
data = {'three': 'three', 'foo': {'one': 1, 'two': 'two'}}
bar = Bar(**data)
assert bar == Bar(three='three', foo=Foo(one=1, two='two'))
data = {'three': 'three', 'foo': Foo(**{'one': 1, 'two': 'two'})}
bar = Bar(**data)
assert bar == Bar(three='three', foo=Foo(one=1, two='two'))
# Use asdict() to work with the dictionary:
bar_dict = asdict(bar)
assert bar_dict == {'three': 'three', 'foo': {'one': 1, 'two': 'two'}}
foo_dict = asdict(bar.foo)
assert foo_dict == {'one': 1, 'two': 'two'}
非常重要的问题不是嵌套,而是值验证/转换。您需要验证值吗?
如果需要值验证,请使用经过良好测试的反序列化库,例如:
pydantic
(更快但凌乱的保留属性,如 schema
干扰来自数据的属性名称。必须重命名和别名 class 属性足以让它烦人)
schematics
(比 pydantic 慢,但类型转换堆栈更成熟)
它们具有惊人的验证和重铸支持并且使用非常广泛(意思是,通常应该运行良好并且不会弄乱您的数据)。但是,它们不是基于 dataclass
的,尽管 Pydantic 包装了 dataclass
功能并允许您从纯数据 classes 切换到 Pydantic 支持的数据 classes 并更改导入声明。
这些库(在该线程中提到)在本地使用数据classes,但验证/类型转换尚未强化。
dacite
validated_dc
如果验证不是特别重要,只需要递归嵌套,像https://gist.github.com/dvdotsenko/07deeafb27847851631bfe4b4ddd9059这样简单的手工代码就足以处理Optional
和List[
Dict[
嵌套模型。
你可以使用 post_init 来完成这个
from dataclasses import dataclass
@dataclass
class One:
f_one: int
f_two: str
@dataclass
class Two:
f_three: str
f_four: One
def __post_init__(self):
self.f_four = One(**self.f_four)
data = {'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}}
print(Two(**data))
# Two(f_three='three', f_four=One(f_one=1, f_two='two'))
如果您同意将此功能与非标准库库 attrs(数据类标准库提供的功能的超集)配对,那么 the cattrs
library 提供了一个处理转换的 structure
函数将本机数据类型转换为数据类,并将自动使用类型注释。
dataclass-wizard 是一个现代选项,也可以为您工作。它支持复杂类型,例如日期和时间、来自 typing
模块的泛型以及 嵌套数据类 结构。
其他“值得拥有”的功能,例如隐式键壳转换 - 即 camelCase 和 TitleCase,这在 API 响应 - 同样支持开箱即用。
PEP 585 and 604 中引入的“新样式”注释可以通过 __future__
导入移植回 Python 3.7,如下所示。
from __future__ import annotations
from dataclasses import dataclass
from dataclass_wizard import fromdict, asdict, DumpMeta
@dataclass
class Two:
f_three: str | None
f_four: list[One]
@dataclass
class One:
f_one: int
f_two: str
data = {'f_three': 'three',
'f_four': [{'f_one': 1, 'f_two': 'two'},
{'f_one': '2', 'f_two': 'something else'}]}
two = fromdict(Two, data)
print(two)
# setup key transform for serialization (default is camelCase)
DumpMeta(key_transform='SNAKE').bind_to(Two)
my_dict = asdict(two)
print(my_dict)
输出:
Two(f_three='three', f_four=[One(f_one=1, f_two='two'), One(f_one=2, f_two='something else')])
{'f_three': 'three', 'f_four': [{'f_one': 1, 'f_two': 'two'}, {'f_one': 2, 'f_two': 'something else'}]}
您可以通过pip
安装数据类向导:
$ pip install dataclass-wizard
我有一个数据类对象,其中嵌套了数据类对象。但是,当我创建主对象时,嵌套对象变成了字典:
@dataclass
class One:
f_one: int
f_two: str
@dataclass
class Two:
f_three: str
f_four: One
Two(**{'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}})
Two(f_three='three', f_four={'f_one': 1, 'f_two': 'two'})
obj = {'f_three': 'three', 'f_four': One(**{'f_one': 1, 'f_two': 'two'})}
Two(**obj)
Two(f_three='three', f_four=One(f_one=1, f_two='two'))
如您所见,只有 **obj
有效。
理想情况下,我想构建我的对象以获得如下内容:
Two(f_three='three', f_four=One(f_one=1, f_two='two'))
除了在访问对象属性时手动将嵌套字典转换为相应的数据类对象之外,还有什么方法可以实现吗?
提前致谢。
这是一个与 dataclasses
模块本身一样复杂的请求,这意味着实现这种“嵌套字段”功能的最佳方法可能是定义一个新的装饰器,类似于 @dataclass
.
幸运的是,如果您不需要 __init__
方法的签名来反映字段及其默认值,例如通过调用 dataclass
呈现的 classes,这可以变得更简单:一个 class 装饰器将调用原始 dataclass
并在其生成的 __init__
方法上包装一些功能,可以使用普通的“...(*args, **kwargs):
”样式来完成功能。
换句话说,所有需要做的就是围绕生成的 __init__
方法编写一个包装器,它将检查传入“kwargs”的参数,检查是否有对应于“dataclass 字段类型”,如果是,则在调用原始 __init__
之前生成嵌套对象。也许这在英语中比在 Python:
from dataclasses import dataclass, is_dataclass
def nested_dataclass(*args, **kwargs):
def wrapper(cls):
cls = dataclass(cls, **kwargs)
original_init = cls.__init__
def __init__(self, *args, **kwargs):
for name, value in kwargs.items():
field_type = cls.__annotations__.get(name, None)
if is_dataclass(field_type) and isinstance(value, dict):
new_obj = field_type(**value)
kwargs[name] = new_obj
original_init(self, *args, **kwargs)
cls.__init__ = __init__
return cls
return wrapper(args[0]) if args else wrapper
请注意,除了不用担心 __init__
签名外,这
也忽略传递 init=False
- 因为无论如何它都没有意义。
(return 行中的 if
负责此工作,无论是使用命名参数调用还是直接作为装饰器,如 dataclass
本身)
并在交互式提示上:
In [85]: @dataclass
...: class A:
...: b: int = 0
...: c: str = ""
...:
In [86]: @dataclass
...: class A:
...: one: int = 0
...: two: str = ""
...:
...:
In [87]: @nested_dataclass
...: class B:
...: three: A
...: four: str
...:
In [88]: @nested_dataclass
...: class C:
...: five: B
...: six: str
...:
...:
In [89]: obj = C(five={"three":{"one": 23, "two":"narf"}, "four": "zort"}, six="fnord")
In [90]: obj.five.three.two
Out[90]: 'narf'
如果你想保留签名,我建议使用 dataclasses
模块本身的私有辅助函数,创建一个新的 __init__
.
我没有编写新的装饰器,而是想出了一个函数,在实际 dataclass
初始化后修改所有 dataclass
类型的字段。
def dicts_to_dataclasses(instance):
"""Convert all fields of type `dataclass` into an instance of the
specified data class if the current value is of type dict."""
cls = type(instance)
for f in dataclasses.fields(cls):
if not dataclasses.is_dataclass(f.type):
continue
value = getattr(instance, f.name)
if not isinstance(value, dict):
continue
new_value = f.type(**value)
setattr(instance, f.name, new_value)
该函数可以手动调用或在__post_init__
中调用。这样 @dataclass
装饰器就可以得到充分利用。
上面的示例调用 __post_init__
:
@dataclass
class One:
f_one: int
f_two: str
@dataclass
class Two:
def __post_init__(self):
dicts_to_dataclasses(self)
f_three: str
f_four: One
data = {'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}}
two = Two(**data)
# Two(f_three='three', f_four=One(f_one=1, f_two='two'))
您可以尝试 dacite
模块。此包简化了从字典创建数据 类 - 它还支持嵌套结构。
示例:
from dataclasses import dataclass
from dacite import from_dict
@dataclass
class A:
x: str
y: int
@dataclass
class B:
a: A
data = {
'a': {
'x': 'test',
'y': 1,
}
}
result = from_dict(data_class=B, data=data)
assert result == B(a=A(x='test', y=1))
要安装英安岩,只需使用 pip:
$ pip install dacite
我已经通过@jsbueno 创建了解决方案的扩充,它也接受 List[<your class/>]
形式的输入。
def nested_dataclass(*args, **kwargs):
def wrapper(cls):
cls = dataclass(cls, **kwargs)
original_init = cls.__init__
def __init__(self, *args, **kwargs):
for name, value in kwargs.items():
field_type = cls.__annotations__.get(name, None)
if isinstance(value, list):
if field_type.__origin__ == list or field_type.__origin__ == List:
sub_type = field_type.__args__[0]
if is_dataclass(sub_type):
items = []
for child in value:
if isinstance(child, dict):
items.append(sub_type(**child))
kwargs[name] = items
if is_dataclass(field_type) and isinstance(value, dict):
new_obj = field_type(**value)
kwargs[name] = new_obj
original_init(self, *args, **kwargs)
cls.__init__ = __init__
return cls
return wrapper(args[0]) if args else wrapper
from dataclasses import dataclass, asdict
from validated_dc import ValidatedDC
@dataclass
class Foo(ValidatedDC):
one: int
two: str
@dataclass
class Bar(ValidatedDC):
three: str
foo: Foo
data = {'three': 'three', 'foo': {'one': 1, 'two': 'two'}}
bar = Bar(**data)
assert bar == Bar(three='three', foo=Foo(one=1, two='two'))
data = {'three': 'three', 'foo': Foo(**{'one': 1, 'two': 'two'})}
bar = Bar(**data)
assert bar == Bar(three='three', foo=Foo(one=1, two='two'))
# Use asdict() to work with the dictionary:
bar_dict = asdict(bar)
assert bar_dict == {'three': 'three', 'foo': {'one': 1, 'two': 'two'}}
foo_dict = asdict(bar.foo)
assert foo_dict == {'one': 1, 'two': 'two'}
非常重要的问题不是嵌套,而是值验证/转换。您需要验证值吗?
如果需要值验证,请使用经过良好测试的反序列化库,例如:
pydantic
(更快但凌乱的保留属性,如schema
干扰来自数据的属性名称。必须重命名和别名 class 属性足以让它烦人)schematics
(比 pydantic 慢,但类型转换堆栈更成熟)
它们具有惊人的验证和重铸支持并且使用非常广泛(意思是,通常应该运行良好并且不会弄乱您的数据)。但是,它们不是基于 dataclass
的,尽管 Pydantic 包装了 dataclass
功能并允许您从纯数据 classes 切换到 Pydantic 支持的数据 classes 并更改导入声明。
这些库(在该线程中提到)在本地使用数据classes,但验证/类型转换尚未强化。
dacite
validated_dc
如果验证不是特别重要,只需要递归嵌套,像https://gist.github.com/dvdotsenko/07deeafb27847851631bfe4b4ddd9059这样简单的手工代码就足以处理Optional
和List[
Dict[
嵌套模型。
你可以使用 post_init 来完成这个
from dataclasses import dataclass
@dataclass
class One:
f_one: int
f_two: str
@dataclass
class Two:
f_three: str
f_four: One
def __post_init__(self):
self.f_four = One(**self.f_four)
data = {'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}}
print(Two(**data))
# Two(f_three='three', f_four=One(f_one=1, f_two='two'))
如果您同意将此功能与非标准库库 attrs(数据类标准库提供的功能的超集)配对,那么 the cattrs
library 提供了一个处理转换的 structure
函数将本机数据类型转换为数据类,并将自动使用类型注释。
dataclass-wizard 是一个现代选项,也可以为您工作。它支持复杂类型,例如日期和时间、来自 typing
模块的泛型以及 嵌套数据类 结构。
其他“值得拥有”的功能,例如隐式键壳转换 - 即 camelCase 和 TitleCase,这在 API 响应 - 同样支持开箱即用。
PEP 585 and 604 中引入的“新样式”注释可以通过 __future__
导入移植回 Python 3.7,如下所示。
from __future__ import annotations
from dataclasses import dataclass
from dataclass_wizard import fromdict, asdict, DumpMeta
@dataclass
class Two:
f_three: str | None
f_four: list[One]
@dataclass
class One:
f_one: int
f_two: str
data = {'f_three': 'three',
'f_four': [{'f_one': 1, 'f_two': 'two'},
{'f_one': '2', 'f_two': 'something else'}]}
two = fromdict(Two, data)
print(two)
# setup key transform for serialization (default is camelCase)
DumpMeta(key_transform='SNAKE').bind_to(Two)
my_dict = asdict(two)
print(my_dict)
输出:
Two(f_three='three', f_four=[One(f_one=1, f_two='two'), One(f_one=2, f_two='something else')])
{'f_three': 'three', 'f_four': [{'f_one': 1, 'f_two': 'two'}, {'f_one': 2, 'f_two': 'something else'}]}
您可以通过pip
安装数据类向导:
$ pip install dataclass-wizard