在Python中创建嵌套的数据类对象

moh*_*666 6 python serialization nested object python-dataclasses

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

@dataclass
class One:
    f_one: int

@dataclass
class One:
    f_one: int
    f_two: str

@dataclass
class Two:
    f_three: str
    f_four: One


data = {'f_three': 'three', 'f_four': {'f_one': 1, 'f_two': 'two'}}

two = Two(**data)

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_2 = Two(**data)

two_2
Two(f_three='three', f_four={'f_one': 1, 'f_two': 'two'})
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,我试图将所有数据作为字典传递,但我没有得到预期的结果.然后我尝试首先构造嵌套对象并将其传递给对象构造函数,但我得到了相同的结果.

理想情况下,我想构建我的对象来得到这样的东西:

Two(f_three='three', f_four=One(f_one=1, f_two='two'))
Run Code Online (Sandbox Code Playgroud)

除了手动将嵌套字典转换为相应的数据类对象,每当访问对象属性时,有没有办法实现其他目的?

提前致谢.

Pet*_*sen 46

您可以为此使用 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'))
Run Code Online (Sandbox Code Playgroud)

  • 不错的答案,但 f_four 期望的是一个 dict 而不是 One 的实例。我们可以像这样使用 Union 类型 [One, dict] (4认同)

jsb*_*eno 9

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

幸运的是,如果一个人不需要__init__方法的签名来反映字段及其默认值,就像通过调用呈现的类一样dataclass,这可以简化得多:一个类装饰器将调用原始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
Run Code Online (Sandbox Code Playgroud)

请注意,除了不担心__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'
Run Code Online (Sandbox Code Playgroud)

如果你想保留签名,我建议使用dataclasses模块本身的私有帮助函数来创建一个新的__init__.

  • 作为记录,`dataclasses.is_dataclass(f.type)` 对于类型为 `List[dataclass]` 的字段返回 false,因此您的装饰器会跳过这些字段。见 /sf/ask/3736326961/?noredirect=1#comment93683831_53376099 (2认同)
  • *更新*:需要这个的人,请检查“pydantic”库 - 我认为它可以处理这个问题,并有足够的代码来提供极端情况。 (2认同)

You*_*uly 9

我没有编写新的装饰器,而是想出了一个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)
Run Code Online (Sandbox Code Playgroud)

该函数可以手动调用或在__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'))
Run Code Online (Sandbox Code Playgroud)


Kon*_*łas 6

您可以尝试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))
Run Code Online (Sandbox Code Playgroud)

要安装dacite,只需使用pip:

$ pip install dacite
Run Code Online (Sandbox Code Playgroud)


Daa*_*tik 5

我通过@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
Run Code Online (Sandbox Code Playgroud)