32

我有一个数据类对象,其中包含嵌套的数据类对象。但是,当我创建主对象时,嵌套对象变成了字典:

@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'))

除了在访问对象属性时手动将嵌套字典转换为相应的数据类对象之外,还有其他方法可以实现吗?

提前致谢。

4

9 回答 9

31

这是一个与dataclasses模块本身一样复杂的请求,这意味着实现这种“嵌套字段”功能的最佳方法可能是定义一个新的装饰器,类似于@dataclass.

幸运的是,如果您不需要__init__方法的签名来反映字段及其默认值,例如调用呈现的类dataclass,这可以简单得多:一个类装饰器,它将调用原始的dataclass 并在其上包装一些功能生成__init__的方法可以使用普通的 " ...(*args, **kwargs):" 样式函数来完成。

换句话说,所有需要做的就是围绕生成的__init__方法编写一个包装器,该包装器将检查“kwargs”中传递的参数,检查是否有任何对应于“数据类字段类型”,如果是,则生成嵌套对象之前调用原来的__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——因为无论如何它都是没有意义的。

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__.

于 2018-07-27T21:39:49.600 回答
16

你可以试试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))

要安装 dacite,只需使用 pip:

$ pip install dacite
于 2018-10-15T12:24:22.163 回答
16

您可以为此使用 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'))
于 2020-12-16T15:12:27.103 回答
12

我没有编写新的装饰器,而是想出了一个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'))
于 2018-09-03T10:56:53.043 回答
6

我通过@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
于 2019-07-08T11:32:13.917 回答
2

如果您可以将此功能与非 stdlib 库 attrs(数据类 stdlib 提供的功能的超集)配对,则cattrs提供了一个structure处理本地数据类型到数据类的转换的函数,并将自动使用类型注释。

于 2021-10-14T18:24:35.120 回答
1

非常重要的问题不是嵌套,而是值验证/转换。您需要验证值吗?

如果需要值验证,请使用经过良好测试的反序列化库,例如:

  • pydantic(更快但混乱的保留属性,例如schema干扰来自数据的属性名称。必须重命名和别名类属性足以使其烦人)
  • schematics(比 pydantic 慢,但更成熟的类型转换堆栈)

它们具有惊人的验证和重铸支持,并且使用非常广泛(意思是,通常应该运行良好并且不会弄乱您的数据)。然而,它们不是dataclass基于的,尽管 Pydantic 包装dataclass了功能并允许您通过更改导入语句从纯数据类切换到 Pydantic 支持的数据类。

这些库(在这个线程中提到)本机使用数据类,但验证/类型转换尚未加强。

  • dacite
  • validated_dc

如果验证不是非常重要,并且只需要递归嵌套,那么简单的手动代码(例如https://gist.github.com/dvdotsenko/07deeafb27847851631bfe4b4ddd9059Optional )就足以处理List[ Dict[嵌套模型。

于 2020-11-25T04:21:30.507 回答
0
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'}

ValidatedDC:https ://github.com/EvgeniyBurdin/validated_dc

于 2020-05-19T09:26:18.037 回答
0

dataclass-wizard是一个现代选项,可以替代地为您工作。它支持复杂的类型,例如日期和时间、typing模块中的泛型以及嵌套的数据类结构。

其他“很高兴拥有”功能,例如隐式密钥大小写转换 - 即在 API 响应中很常见的camelCaseTitleCase - 也同样支持开箱即用。

PEP 585604中引入的“新样式”注释可以通过__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'}]}

您可以通过以下方式安装 Dataclass Wizard pip

$ pip install dataclass-wizard
于 2021-12-09T01:02:39.850 回答