带有列表注释的嵌套 python 个数据类

Nested python dataclasses with list annotations

python^3.7。尝试创建嵌套数据 classes 以处理复杂的 json 响应。我设法通过为 json 的每个级别创建数据 class 并使用 __post_init_ 将字段设置为其他数据 classes 的对象来做到这一点。然而,这会创建大量样板代码,而且嵌套对象也没有注释。

这个答案帮助我更接近使用包装器的解决方案:

但是对于属性是对象列表的情况,它并没有解决。 some_attribute: List[SomeClass]

这是与我的数据相似的示例:

from dataclasses import dataclass, is_dataclass
from typing import List
from copy import deepcopy

# decorator from the linked thread:
def nested_deco(*args, **kwargs):
    def wrapper(check_class):

        # passing class to investigate
        check_class = dataclass(check_class, **kwargs)
        o_init = check_class.__init__

        def __init__(self, *args, **kwargs):

            for name, value in kwargs.items():

                # getting field type
                ft = check_class.__annotations__.get(name, None)

                if is_dataclass(ft) and isinstance(value, dict):
                    obj = ft(**value)
                    kwargs[name] = obj
                o_init(self, *args, **kwargs)

        check_class.__init__ = __init__

        return check_class

    return wrapper(args[0]) if args else wrapper


#some dummy dataclasses to resemble my data structure

@dataclass
class IterationData:
    question1: str
    question2: str


@nested_deco
@dataclass
class IterationResult:
    name: str
    data: IterationData


@nested_deco
@dataclass
class IterationResults:
    iterations: List[IterationResult]


@dataclass
class InstanceData:
    date: str
    owner: str


@nested_deco
@dataclass
class Instance:
    data: InstanceData
    name: str


@nested_deco
@dataclass
class Result:
    status: str
    iteration_results: IterationResults


@nested_deco
@dataclass
class MergedInstance:
    instance: Instance
    result: Result


#example data

single_instance = {
    "instance": {
        "name": "example1",
        "data": {
            "date": "2021-01-01",
            "owner": "Maciek"
        }
    },
    "result": {
        "status": "complete",
        "iteration_results": [
            {
                "name": "first",
                "data": {
                    "question1": "yes",
                    "question2": "no"
                }
            }
        ]
    }
}

instances = [deepcopy(single_instance) for i in range(3)] #created a list just to resemble mydata
objres = [MergedInstance(**inst) for inst in instances]

如您所见。 nested_deco 完美适用于 MergedInstance 的属性和 Instance 的属性 data 但它不会在 iteration_results 上加载 IterationResults class Result.

有没有办法实现?

我还附上了我的 post_init 解决方案的示例,它创建了 classes 的对象,但没有属性注释:

@dataclass
class IterationData:
    question1: str
    question2: str


@dataclass
class IterationResult:
    name: str
    data: dict

    def __post_init__(self):
        self.data = IterationData(**self.data)


@dataclass
class InstanceData:
    date: str
    owner: str


@dataclass
class Instance:
    data: dict
    name: str

    def __post_init__(self):
        self.data = InstanceData(**self.data)


@dataclass
class Result:
    status: str
    iteration_results: list

    def __post_init__(self):
        self.iteration_results = [IterationResult(**res) for res in self.iteration_results]


@dataclass
class MergedInstance:
    instance: dict
    result: dict

    def __post_init__(self):
        self.instance = Instance(**self.instance)
        self.result = Result(**self.result)

这并没有真正回答你关于嵌套装饰器的问题,但我最初的建议是通过使用以前解决过同样问题的库来避免为自己做很多艰苦的工作。

有很多像 pydantic which also provides data validation and is something I might recommend. If you are interested in keeping your existing dataclass structure and not wanting to inherit from anything, you can use libraries such as dataclass-wizard and dataclasses-json 这样众所周知的。后者提供了一种您可能感兴趣的装饰器方法。但理想情况下,目标是找到一个(高效的)JSON 序列化库,它已经提供了您所需要的。

这是一个使用 dataclass-wizard 库的示例,只需进行最少的更改(无需从 mixin class 继承)。请注意,我必须稍微修改您的输入 JSON 对象,否则它与数据 class 模式不完全匹配。但除此之外,它看起来应该按预期工作。我还删除了 copy.deepcopy,因为它有点慢,我们不需要它(辅助函数不会直接修改 dict 对象,这很简单,可以测试)

from dataclasses import dataclass
from typing import List

from dataclass_wizard import fromlist


@dataclass
class IterationData:
    question1: str
    question2: str


@dataclass
class IterationResult:
    name: str
    data: IterationData


@dataclass
class IterationResults:
    iterations: List[IterationResult]


@dataclass
class InstanceData:
    date: str
    owner: str


@dataclass
class Instance:
    data: InstanceData
    name: str


@dataclass
class Result:
    status: str
    iteration_results: IterationResults


@dataclass
class MergedInstance:
    instance: Instance
    result: Result


single_instance = {
    "instance": {
        "name": "example1",
        "data": {
            "date": "2021-01-01",
            "owner": "Maciek"
        }
    },
    "result": {
        "status": "complete",
        "iteration_results": {
            # Notice i've changed this here - previously syntax was invalid (this was
            # a list)
            "iterations": [
                {
                    "name": "first",
                    "data": {
                        "question1": "yes",
                        "question2": "no"
                    }
                }
            ]
        }
    }
}

instances = [single_instance for i in range(3)]  # created a list just to resemble mydata

objres = fromlist(MergedInstance, instances)

for obj in objres:
    print(obj)

使用 dataclasses-json 库:

from dataclasses import dataclass
from typing import List

from dataclasses_json import dataclass_json


# Same as above
...

@dataclass_json
@dataclass
class MergedInstance:
    instance: Instance
    result: Result


single_instance = {...}

instances = [single_instance for i in range(3)]  # created a list just to resemble mydata

objres = [MergedInstance.from_dict(inst) for inst in instances]

for obj in objres:
    print(obj)

奖励: 假设您正在调用一个 API,您 returns 是一个复杂的 JSON 响应,例如上面的响应。如果你想将这个 JSON 响应转换为数据 class 模式,通常你必须手写它,如果 JSON 的结构,这可能有点累人特别复杂。

如果有一种方法可以简化嵌套数据class 结构的生成,那不是很酷吗? dataclass-wizard 库带有一个接受任意 JSON 输入的 CLI 工具,因此在给定这样的输入的情况下自动生成数据 class 模式应该是可行的。

假设您在 testing.json 文件中有这些内容:

{
    "instance": {
        "name": "example1",
        "data": {
            "date": "2021-01-01",
            "owner": "Maciek"
        }
    },
    "result": {
        "status": "complete",
        "iteration_results": {
            "iterations": [
                {
                    "name": "first",
                    "data": {
                        "question1": "yes",
                        "question2": "no"
                    }
                }
            ]
        }
    }
}

然后我们运行下面的命令:

wiz gs testing testing

以及我们新 testing.py 文件的内容:

from dataclasses import dataclass
from datetime import date
from typing import List, Union

from dataclass_wizard import JSONWizard


@dataclass
class Data(JSONWizard):
    """
    Data dataclass

    """
    instance: 'Instance'
    result: 'Result'


@dataclass
class Instance:
    """
    Instance dataclass

    """
    name: str
    data: 'Data'


@dataclass
class Data:
    """
    Data dataclass

    """
    date: date
    owner: str


@dataclass
class Result:
    """
    Result dataclass

    """
    status: str
    iteration_results: 'IterationResults'


@dataclass
class IterationResults:
    """
    IterationResults dataclass

    """
    iterations: List['Iteration']


@dataclass
class Iteration:
    """
    Iteration dataclass

    """
    name: str
    data: 'Data'


@dataclass
class Data:
    """
    Data dataclass

    """
    question1: Union[bool, str]
    question2: Union[bool, str]

这似乎或多或少与原始问题中的相同嵌套数据class结构匹配,最重要的是我们不需要自己编写任何代码!

但是,有一个小问题 - 由于一些重复的 JSON 键,我们最终得到三个名为 Data 的数据 class。所以我继续将它们重命名为 Data1Data2Data3 以确保唯一性。然后我们可以进行快速测试以确认我们能够将相同的 JSON 数据加载到我们的新数据 class 模式中:

import json
from dataclasses import dataclass
from datetime import date
from typing import List, Union

from dataclass_wizard import JSONWizard


@dataclass
class Data1(JSONWizard):
    """
    Data dataclass

    """
    instance: 'Instance'
    result: 'Result'


@dataclass
class Instance:
    """
    Instance dataclass

    """
    name: str
    data: 'Data2'


@dataclass
class Data2:
    """
    Data dataclass

    """
    date: date
    owner: str


@dataclass
class Result:
    """
    Result dataclass

    """
    status: str
    iteration_results: 'IterationResults'


@dataclass
class IterationResults:
    """
    IterationResults dataclass

    """
    iterations: List['Iteration']


@dataclass
class Iteration:
    """
    Iteration dataclass

    """
    name: str
    data: 'Data3'


@dataclass
class Data3:
    """
    Data dataclass

    """
    question1: Union[bool, str]
    question2: Union[bool, str]


# ---- Start of our test

with open('testing.json') as in_file:
    d = json.load(in_file)

c = Data1.from_dict(d)

print(repr(c))
# Data1(instance=Instance(name='example1', data=Data2(date=datetime.date(2021, 1, 1), owner='Maciek')), result=Result(status='complete', iteration_results=IterationResults(iterations=[Iteration(name='first', data=Data3(question1='yes', question2='no'))])))

使用dacitefrom_dict。这是处理 nested 数据类所需要的。

from dataclasses import dataclass
from dacite import from_dict


@dataclass
class User:
    name: str
    age: int
    is_active: bool


data = {
    'name': 'John',
    'age': 30,
    'is_active': True,
}

user = from_dict(data_class=User, data=data)

您实际上可以直接在定义中嵌套数据类,而且效果很好。看一下我的 post,我曾在其中尝试解决一个类似的问题:Python nested dataclasses ...is this valid?

或者您可以定义一个 'child' 数据类,并将其作为 'parent' 容器数据类中元素的类型。

我今天仍然在生产代码中使用这种方法,而且效果很好(我也使用数据类-json,正如有人提到的,用于 json 序列化并进行一致性验证。

我还扭曲了嵌套数据类以允许根据它们的定义导出 json-schemas。 ...不简单但可行。 (对于我们的用例 - 导出数据以供 NodeJS 应用程序导入,需要 json 模式)。


然而正如第一个回复提到的,有一个更好的方法(可能在你的情况下)——使用pydantic。如果您几乎是从头开始,我建议您这样做。

在我的待办事项列表中,我们的生产代码将其重构为使用 pydantic 而不是嵌套数据类:嵌套数据类确实有效,您可以让它们进行 json 序列化和自我- 根据定义的类型进行验证。 ...但这有点痛苦恕我直言。

这就是 pydantic 的设计目的,而且(再次恕我直言)它开箱即用地更简单、更清晰。