【问题标题】:Nested python dataclasses with list annotations带有列表注释的嵌套 python 数据类
【发布时间】:2021-11-06 16:57:15
【问题描述】:

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

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

https://stackoverflow.com/a/51565863/8325015

但是它不能解决属性是对象列表的情况。 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_resultsResult 上加载 IterationResults 类。

有办法实现吗?

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

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

【问题讨论】:

    标签: python json nested wrapper python-dataclasses


    【解决方案1】:

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

    有很多众所周知的,比如pydantic,它也提供数据验证,我可能会推荐它。如果您有兴趣保留现有的dataclass 结构并且不想从任何东西继承,您可以使用诸如dataclass-wizarddataclasses-json 之类的库。后者提供了一种您可能感兴趣的装饰器方法。但理想情况下,目标是找到一个(高效的)JSON 序列化库,它已经提供了您所需要的。

    这是一个使用 dataclass-wizard 库的示例,只需进行最少的更改(无需从 mixin 类继承)。请注意,我必须稍微修改您的输入 JSON 对象,否则它与数据类模式不完全匹配。但除此之外,它看起来应该按预期工作。我还删除了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,该 API 会返回一个复杂的 JSON 响应,例如上面的那个。如果要将此 JSON 响应转换为数据类模式,通常必须手动将其写出来,如果 JSON 的结构特别复杂,这可能会有点累。

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

    假设您在 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]
    

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

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

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

    【讨论】:

      【解决方案2】:

      使用dacitefrom_dict。这是您处理嵌套数据类所需要的。

      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)
      

      【讨论】:

        【解决方案3】:

        您实际上可以将数据类直接嵌套在定义中,而且效果相当好。看看我的一个帖子,不久前我试图解决一个类似的问题:Python nested dataclasses ...is this valid?

        或者您可以定义一个“子”数据类,并将其作为“父”容器数据类中元素的类型。

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

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


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

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

        这就是 pydantic 的设计初衷,并且(再次恕我直言)开箱即用地更简单、更干净。

        【讨论】:

        • 同意,pydantic 是一个很好的解决方案,但是我记得在某处提到的唯一问题是它引入了包膨胀 - 例如。您可能不需要验证,甚至不需要替换数据类。我也喜欢dataclasses-json,因为它提供的内容以及好的文档,但重要的是要注意它引入了您可能不需要的其他第 3 方依赖项(例如棉花糖),因此它比 pydantic 慢。如果个人可以选择,我会建议使用 pydantic 而不是 dataclasses-json - 如果不是很明显,我一般不是棉花糖的粉丝。
        猜你喜欢
        • 2023-03-20
        • 2012-12-21
        • 2011-06-04
        • 1970-01-01
        • 2021-01-12
        • 2021-07-13
        • 1970-01-01
        • 1970-01-01
        • 2021-02-11
        相关资源
        最近更新 更多