【问题标题】:Loading nested objects in yaml files with custom constructors使用自定义构造函数在 yaml 文件中加载嵌套对象
【发布时间】:2020-04-22 22:13:44
【问题描述】:

我有一个 yaml 文件,描述了我正在导入的一系列嵌套对象。因为我想在每次调用时都有__init__,所以我正在编写自定义构造函数

def volume_constructor(loader, node):
    instance = Volume.__new__(Volume)
    yield instance
    state = loader.construct_mapping(node)
    instance.__init__(**state)

然后,当我读取 yaml 文件时,我会在加载之前添加构造函数:

yaml.add_constructor(volume_yaml_tag, volume_constructor, yaml.SafeLoader)
yaml.add_constructor(host_yaml_tag, volume_constructor, yaml.SafeLoader)
configcontent = configfile.read()
cfg = yaml.safe_load(configcontent)

在这种情况下,卷是父对象,它包含几个属性,如普通字符串等,以及一个主机对象列表。下面的摘录显示了为简洁起见省略了值的一般形式

- !!python/object:libraries.volume.Volume
_Volume__consuminghostlist:
- !!python/object:libraries.host.Host
  _Host__property1: fc
  _Host__property2: localhost
_Volume__property1: false
_Volume__property2: sometextvalue
_Volume__property3: somenumericvalue

我可以毫无问题地定义和转储对象,但是当我尝试使用上面定义的自定义构造函数进行加载时,我得到下面的错误堆栈。请注意,当我更改属性的顺序以便处理的第一个不是嵌套对象时,错误仍然会标记第一个处理的对象。

        cfg = yaml.safe_load(configcontent)
  File "....yaml\__init__.py", line 162, in safe_load
    return load(stream, SafeLoader)
  File "...yaml\__init__.py", line 114, in load
    return loader.get_single_data()
  File "...yaml\constructor.py", line 43, in get_single_data
    return self.construct_document(node)
  File "...yaml\constructor.py", line 52, in construct_document
    for dummy in generator:
  File "...volume.py", line 15, in volume_constructor
    instance.__init__(**state)
TypeError: __init__() got an unexpected keyword argument '_Volume__consuminghostlist'

但是,如果我放弃添加自定义构造函数并做一个股票loadload 是成功的,尽管此时不再调用每个 __init__。我应该以另一种方式调用__init__,还是构造函数的设置方式有问题?我想必须有某种方法可以避免手动解析构造函数上的每个标记并将它们作为单独的参数提供给 init_。我玩弄了设置deepcopy 属性,但没有任何区别。作为参考,配置文件是使用以下方法创建的:

yaml.dump(cfg, sys.stdout, Dumper=noalias_dumper, default_flow_style=False)

更新: 一些回复提到了无效的 yaml 格式,我同意这看起来不正确,所以我做了一些实验来看看为什么会发生这种情况。我通过定义对象然后转储它们而不是手动编写来创建 yaml,因此我希望转储使用有效的 yaml 格式。我发现如下所示,当我为属性设置@property 装饰器时,这会被破坏

import yaml
import sys
class Volume(yaml.YAMLObject):
    yaml_loader = yaml.SafeLoader

    def __init__(self, input_property1, input_property2):
        self.property1 = input_property1
        self.property2 = input_property2

    @property
    def property1(self):
        return self.__property1

    @property1.setter
    def property1(self, input_property1):
        self.__property1 = input_property1


samplevolume = Volume("ABC", 1.0)
noalias_dumper = yaml.dumper.Dumper
noalias_dumper.ignore_aliases = lambda self, data: True
yaml.dump(samplevolume, sys.stdout, Dumper=noalias_dumper, default_flow_style=False)

当我运行上面的示例代码时,我得到下面的转储输出,显示了对没有@property 定义的属性的预期处理

!!python/object:__main__.Volume
_Volume__property1: ABC
property2: 1.0

更新 2 根据与@flyx 的讨论,我尝试将名称对齐,如下所示

import yaml
import sys
class Volume(yaml.YAMLObject):
    yaml_loader = yaml.SafeLoader

    def __init__(self, property1, property2):
        self.property1 = property1
        self.property2 = property2

    @property
    def property1(self):
        return self.property1

    @property1.setter
    def property1(self, property1):
        self.__property1 = property1


samplevolume = Volume("ABC", 1.0)
noalias_dumper = yaml.dumper.Dumper
noalias_dumper.ignore_aliases = lambda self, data: True
yaml.dump(samplevolume, sys.stdout, Dumper=noalias_dumper, default_flow_style=False, allow_unicode=True)

但它不会改变输出格式

!!python/object:__main__.Volume
_Volume__property1: ABC
property2: 1.0

鉴于使用属性装饰器是 python 的核心功能,我很惊讶 pyyaml 需要自定义表示器和构造器来处理它。

【问题讨论】:

  • 请创建一个minimal, reproducible example。您显示的摘录不是有效的 YAML,因此不清楚发生了什么,并且您的声明 „省略了值“ 没有帮助; Volume 的缺失定义也没有。
  • @flyx 我在原始帖子中进行了更新,探讨了为什么 yaml 格式很奇怪。简而言之,我使用转储程序而不是手动创建它,并且属性装饰器的使用似乎导致了奇怪的格式。

标签: python yaml pyyaml ruamel.yaml


【解决方案1】:

您显示的类Volume 有一个__init__,需要两个参数:input_property1input_property2。但是,默认表示器表示名称为_Volume__property1 的第一个属性。在您对__init__ 的调用中,它无法映射,因为它与__init__ 的参数名称不对应。

所以你的选择是:

  • 编辑__init__ 的参数名称,使其与序列化参数的名称相匹配
  • 添加自定义表示器,用__init__ 所期望的字段名称表示对象
  • 使用YAMLObject 的默认加载程序并将您的自定义代码放入您以后调用的正常方法中。例如:
def volume_constructor(loader, node):
    instance = loader.construct_yaml_object(loader, Volume)
    instance.myinit()
    return instance

这样,加载Volume 与默认转储程序兼容,因为您使用的是默认加载程序。然后将任何额外的初始化代码放入方法myinit

【讨论】:

  • 对齐名称(property1 与 input_property1)并没有解决上述第二次更新中提到的问题。我本来希望 pyyaml 能够处理属性装饰器,因为它们是 Python 的核心概念。现在,我正在权衡为所有内容编写自定义表示器,而不是让我与无效格式和平相处,因为它可以工作并在执行安全加载后克隆对象,以便 init 被正确调用
  • 问题不在于 PyYAML。如果您使用标准表示器和构造器,PyYAML 可以与您的类完美配合,就像我提出的第三个解决方案一样。如果要对齐名称,正确的参数名称是 _Volume__property1,而不是 property1,正如您在 YAML 输出中看到的那样。这是由private name mangling 引起的,是标准的 Python 功能。如果你想有一个自定义的构造函数但保留标准的表示器,你必须遵守规则。
【解决方案2】:

您不会在Volume 类的__init__ 中处理关键字_Volume__consuminghostlist,当然您需要这样做。

您的volume_constructor 假定标记节点是一个映射(因为您在该函数中调用construct_mapping)。尽管您的 YAML 无效,但很明显的一点是,上述映射有一个键 _Volume__consuminghostlist。因此,python 字典 state 将拥有该密钥,并且由于您使用 ** 将其传递给 __init__,这将像您调用一样工作:

__init__(_Volume__consuminghostlist: <value for _Volume__consuminghostlist>)

而且你必须注意那个关键字。

如果您以这种方式执行操作,那么在 ruamel.yaml 和 PyYAML 中都是必需的。

【讨论】:

  • @Athon 我在原始帖子中进行了更新,探讨了为什么 yaml 格式很奇怪。简而言之,我使用转储程序而不是手动创建它,并且属性装饰器的使用似乎导致了奇怪的格式。
猜你喜欢
  • 1970-01-01
  • 2020-02-24
  • 2011-05-17
  • 2012-04-29
  • 1970-01-01
  • 1970-01-01
  • 2018-02-27
  • 1970-01-01
  • 2018-09-04
相关资源
最近更新 更多