当ruamel.yaml从字符串加载@dataclass时,不会调用__post_init__

时间:2018-07-26 00:43:07

标签: python yaml ruamel.yaml

假设我创建了一个@dataclass class Foo,并添加了一个__post_init__以进行类型检查和处理。

当我尝试yaml.load对象!Foo时,__post_init__没有被调用。

from dataclasses import dataclass, fields

from ruamel.yaml import yaml_object, YAML


yaml = YAML()


@yaml_object(yaml)
@dataclass
class Foo:
    foo: int
    bar: int

    def __post_init__(self):
        raise Exception
        for field in fields(self):
            value = getattr(self, field.name)
            typ = field.type
            if not isinstance(value, typ):
                raise Exception

s = '''\
!Foo
foo: "foo"
bar: "bar"
'''
yaml.load(s)

通过ruamel.yaml加载数据类时如何执行参数检查?

此行为在Python 3.7以及pip install dataclasses的3.6中发生。

2 个答案:

答案 0 :(得分:2)

我不确定这是否是正确解决方法...

我可以将逻辑从__post_init__移到__setstate__(state: dict)YAML().load()会调用它。

def __setstate__(self, state):
    self.__dict__.update(state)
    # I could call self.__post_init__(), or alternatively move logic here:
    for field in fields(self):
        value = getattr(self, field.name)
        typ = field.type
        if not isinstance(value, typ):
            raise Exception

YAML().load(s)调用Foo.__setstate__(state)(如果存在),但显然不__init__(调用__post_init__)。这是故意设计的决定吗?

答案 1 :(得分:2)

之所以没有调用__post_init__是因为ruamel.yaml(及其Constructor中的PyYAML代码)是在创建dataclasses之前就创建的。

当然可以将调用__post_init_()的代码添加到ruamel.yaml的Python对象构造函数中,最好在测试是否使用@dataclass创建了某些东西之后,否则恰好有一个名为__post_init_的方法的Data-Class类,将突然在加载过程中调用该方法。

如果没有这样的类,则可以在使用YAML()进行第一次加载/转储(此时实例化构造函数)之前,将自己更聪明的构造函数添加到yaml.Constructor = MyConstructor实例中。但是添加构造函数并不像继承RoundTripConstructor那样简单,因为所有支持的节点类型都需要在这种新的构造函数类型上注册。

大多数时候,我发现在RoundTripConstructor上修补适当的方法会更容易:

from dataclasses import dataclass, fields
from ruamel.yaml import yaml_object, YAML, RoundTripConstructor


def my_construct_yaml_object(self, node, cls):
    for data in self.org_construct_yaml_object(node, cls):
      yield data
    # not doing a try-except, in case `__post_init__` does catch the AttributeError
    post_init = getattr(data, '__post_init__', None)
    if post_init:
        post_init()

RoundTripConstructor.org_construct_yaml_object = RoundTripConstructor.construct_yaml_object
RoundTripConstructor.construct_yaml_object = my_construct_yaml_object

yaml = YAML()
yaml.preserve_quotes = True

@yaml_object(yaml)
@dataclass
class Foo:
    foo: int
    bar: int

    def __post_init__(self):
        for field in fields(self):
            value = getattr(self, field.name)
            typ = field.type
            if not isinstance(value, typ):
                raise Exception

s = '''\
!Foo
foo: "foo"
bar: "bar"
'''
d = yaml.load(s)

引发异常:

Traceback (most recent call last):
  File "try.py", line 36, in <module>
    d = yaml.load(s)
  File "/home/venv/tmp-46489abf428c4cd4/lib/python3.7/site-packages/ruamel/yaml/main.py", line 266, in load
    return constructor.get_single_data()
  File "/home/venv/tmp-46489abf428c4cd4/lib/python3.7/site-packages/ruamel/yaml/constructor.py", line 105, in get_single_data
    return self.construct_document(node)
  File "/home/venv/tmp-46489abf428c4cd4/lib/python3.7/site-packages/ruamel/yaml/constructor.py", line 115, in construct_document
    for dummy in generator:
  File "try.py", line 10, in my_construct_yaml_object
    post_init()
  File "try.py", line 29, in __post_init__
    raise Exception
Exception

请注意,YAML中的双引号是多余的,因此,如果要在往返行程中保留双引号,则需要执行yaml.preserve_quotes = True