__post_init__ классов данных python 3.x не вызывается при загрузке из yaml

Обратите внимание, что я уже упоминал вопрос StackOverflow здесь. Я публикую этот вопрос, чтобы выяснить, безопасно ли вызывать __post_init__. Пожалуйста, проверьте вопрос до конца.

Проверьте приведенный ниже код. На шаге 3 мы загружаем dataclass A из строки yaml. Обратите внимание, что он не вызывает метод __post_init__.

import dataclasses
import yaml


@dataclasses.dataclass
class A:
    a: int = 55

    def __post_init__(self):
        print("__post_init__ got called", self)


print("\n>>>>>>>>>>>> 1: create dataclass object")
a = A(33)
print(a)  # print dataclass
print(dataclasses.fields(a))

print("\n>>>>>>>>>>>> 2: dump to yaml")
s = yaml.dump(a)
print(s)  # print yaml repr

print("\n>>>>>>>>>>>> 3: create class from str")
a_ = yaml.load(s)
print(a_)  # print dataclass loaded from yaml str
print(dataclasses.fields(a_))

Решение, которое я вижу на данный момент, это вызов __-post_init__ самостоятельно в конце, как в приведенном ниже фрагменте кода.

a_.__post_init__()

Я не уверен, что это безопасное воссоздание yaml сериализованного dataclass. Кроме того, это создаст проблему, когда __post_init__ принимает kwargs в случае, когда dataclass поля имеют тип dataclasses.InitVar.


person Praveen Kulkarni    schedule 24.01.2020    source источник
comment
Что, если вы загрузите что-то, что не реализует __post_init__()? Могу я спросить вас, почему вы используете YAML для сохранения объектов вместо рассола?   -  person Error - Syntactical Remorse    schedule 24.01.2020
comment
Все работает нормально, если я не реализую __post_init__(), но я хочу, чтобы он выполнял некоторые действия при загрузке класса данных. Я использую yaml вместо pickle. Я хочу создать API для отдыха с чистым интерфейсом yaml в будущем.   -  person Praveen Kulkarni    schedule 25.01.2020


Ответы (1)


Это поведение работает по назначению. Вы сбрасываете существующий объект, поэтому при его загрузке pyyaml ​​намеренно избегает повторной инициализации объекта. Прямые атрибуты выгруженного объекта будут сохранены, даже если они созданы в __post_init__, потому что эта функция запускается до дампа. Если вам нужны побочные эффекты, исходящие от __post_init__, например оператор печати в вашем примере, вам нужно убедиться, что происходит инициализация.

Есть несколько способов сделать это. Вы можете использовать подходы либо с метаклассом, либо с добавлением конструктора/представителя, описанные в документации pyyaml. Вы также можете вручную изменить строку дампа в вашем примере, чтобы она была «!! python/object/new:» вместо «!! python/object:». Если ваша конечная цель состоит в том, чтобы сгенерировать файл yaml другим способом, то это может быть решением.

См. ниже обновление вашего кода, использующего подход метакласса и вызывающего __post_init__ при загрузке из выгруженного объекта класса. Вызов cls(**fields) в from_yaml обеспечивает инициализацию объекта. yaml.load использует cls.__new__ для создания объектов с тегом ''!!python/object:', а затем вручную загружает сохраненные атрибуты в объект.

import dataclasses
import yaml


@dataclasses.dataclass
class A(yaml.YAMLObject):
    a: int = 55

    def __post_init__(self):
        print("__post_init__ got called", self)

    yaml_tag = '!A'
    yaml_loader = yaml.SafeLoader

    @classmethod
    def from_yaml(cls, loader, node):
        fields = loader.construct_mapping(node, deep=True)
        return cls(**fields)

print("\n>>>>>>>>>>>> 1: create dataclass object")
a = A(33)
print(a)  # print dataclass
print(dataclasses.fields(a))

print("\n>>>>>>>>>>>> 2: dump to yaml")
s = yaml.dump(a)
print(s)  # print yaml repr

print("\n>>>>>>>>>>>> 3: create class from str")
a_ = yaml.load(s, Loader=A.yaml_loader)
print(a_)  # print dataclass loaded from yaml str
print(dataclasses.fields(a_))
person JustBob81    schedule 28.03.2020