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

时间:2018-07-27 20:03:48

标签: python object serialization nested python-dataclasses

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

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

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

理想情况下,我想构造我的对象以得到如下内容:

Two(f_three='three', f_four=One(f_one=1, f_two='two'))

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

谢谢。

7 个答案:

答案 0 :(得分:7)

这是一个复杂度与dataclasses模块本身的复杂度匹配的请求:这意味着实现此“嵌套字段”功能的最佳方法可能是定义一个类似于{{1 }}。

幸运的是,如果不需要@dataclass方法的签名来反映字段及其默认值,例如通过调用__init__呈现的类,则可以简化很多:A类装饰器将调用原始的dataclass并在其生成的dataclass方法上包装一些功能,可以使用简单的“ __init__”样式函数来实现。

换句话说,所有要做的就是包装生成的...(*args, **kwargs):方法,该方法将检查在“ kwargs”中传递的参数,检查是否有任何对应于“数据类字段类型”的参数,如果是, ,请在调用原始__init__之前生成嵌套对象。也许用英语比用Python拼写更难:

__init__

请注意,除了不必担心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

答案 1 :(得分:4)

您可以尝试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

答案 2 :(得分:2)

在实际的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'))

答案 3 :(得分:0)

我通过@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

答案 4 :(得分: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

答案 5 :(得分:0)

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

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

  • pydanticschema之类的速度更快但混乱的保留属性会干扰来自数据的属性名称。必须对类属性进行重命名和别名处理才能使它烦人)
  • schematics(比pydantic慢,但更成熟的类型转换堆栈)

它们具有出色的验证和重铸支持,并且得到了广泛的使用(这意味着,它们通常应该可以正常工作,并且不会弄乱您的数据)。但是,它们并不基于dataclass,尽管Pydantic封装了dataclass功能,并允许您通过更改import语句从纯数据类切换到Pydantic支持的数据类。

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

  • dacite
  • validated_dc

如果验证不是非常重要,并且仅需要递归嵌套,那么像https://gist.github.com/dvdotsenko/07deeafb27847851631bfe4b4ddd9059这样的简单手工滚动代码就足以处理嵌套的OptionalList[ Dict[模型。

答案 6 :(得分:0)

您可以为此使用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'))
相关问题