【问题标题】:Creating nested dataclass objects in Python在 Python 中创建嵌套的数据类对象
【发布时间】:2019-01-04 23:38:17
【问题描述】:

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

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

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

提前致谢。

【问题讨论】:

  • 如果您实际使用obj,您的第二种方法就可以了。 Two(**obj) 给我Two(f_three='three', f_four=One(f_one=1, f_two='two'))
  • 感谢您指出我的错误。知道是否可以使用第一种方法实现相同的结果?如果您的数据类对象中有多个嵌套对象,则第二种方法似乎太乏味了。

标签: python object serialization nested python-dataclasses


【解决方案1】:

这是一个与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__

【讨论】:

  • 你已经用装饰器链接了 attrs。太棒了。
  • 确实——我认为这个 sn-p 可能值得拥有一个 Pypi 模块。我看到它已经发布了。
  • 作为记录,dataclasses.is_dataclass(f.type)List[dataclass] 类型的字段返回 false,因此您的装饰器会跳过这些字段。见stackoverflow.com/questions/53376099/…
  • 请注意,此地址有效,但是,数据类在内部使用 deepcopy(),这在处理大型对象的数据灭菌时会显着减慢速度。
  • 更新:需要这个的人,请检查“pydantic”库——我认为它可以处理这个问题,有足够的代码来提供极端情况。
【解决方案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】:

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

    【讨论】:

      【解决方案4】:

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

      【讨论】:

      • 使用你的装饰器我得到: AttributeError: type object 'list' has no attribute 'origin' 如果数据类属性之一被注释 List[SomeClass]
      【解决方案5】:
      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'}
      

      验证DC:https://github.com/EvgeniyBurdin/validated_dc

      【讨论】:

        【解决方案6】:

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

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

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

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

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

        • dacite
        • validated_dc

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

        【讨论】:

          【解决方案7】:

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

          【讨论】:

          • 不错,简单,没有外部依赖。
          • 不错的答案,但 f_four 期望的是 dict 而不是 One 的实例。我们可以像这样使用 Union 类型 [One, dict]
          【解决方案8】:

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

          【讨论】:

            【解决方案9】:

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

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

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

            您可以通过pip安装Dataclass Wizard:

            $ pip install dataclass-wizard
            

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2022-12-23
              • 2022-08-05
              • 2014-01-10
              • 1970-01-01
              • 1970-01-01
              • 2019-12-24
              • 1970-01-01
              相关资源
              最近更新 更多