为啥 PyYAML 使用生成器来构造对象?

Posted

技术标签:

【中文标题】为啥 PyYAML 使用生成器来构造对象?【英文标题】:Why does PyYAML use generators to construct objects?为什么 PyYAML 使用生成器来构造对象? 【发布时间】:2017-06-13 12:53:16 【问题描述】:

我一直在阅读 PyYAML 源代码,试图了解如何定义一个合适的构造函数,我可以使用 add_constructor 添加该构造函数。我现在对该代码的工作原理有了很好的理解,但我仍然不明白为什么 SafeConstructor 中的默认 YAML 构造函数是生成器。比如SafeConstructor的方法construct_yaml_map

def construct_yaml_map(self, node):
    data = 
    yield data
    value = self.construct_mapping(node)
    data.update(value)

我了解生成器如何在BaseConstructor.construct_object 中使用,如下所示以存根一个对象,并且仅在将deep=False 传递给construct_mapping 时才使用来自节点的数据填充它:

    if isinstance(data, types.GeneratorType):
        generator = data
        data = generator.next()
        if self.deep_construct:
            for dummy in generator:
                pass
        else:
            self.state_generators.append(generator)

并且我了解在deep=Falseconstruct_mapping 的情况下,BaseConstructor.construct_document 中的数据是如何生成的。

def construct_document(self, node):
    data = self.construct_object(node)
    while self.state_generators:
        state_generators = self.state_generators
        self.state_generators = []
        for generator in state_generators:
            for dummy in generator:
                pass

我不明白的是,将数据对象存根并通过迭代 construct_document 中的生成器来处理对象的好处。是否必须这样做以支持 YAML 规范中的某些内容,还是提供性能优势?

This answer on another question 有点帮助,但我不明白为什么这个答案会这样:

def foo_constructor(loader, node):
    instance = Foo.__new__(Foo)
    yield instance
    state = loader.construct_mapping(node, deep=True)
    instance.__init__(**state)

而不是这个:

def foo_constructor(loader, node):
    state = loader.construct_mapping(node, deep=True)
    return Foo(**state)

我已经测试过后一种形式适用于发布在另一个答案上的示例,但也许我错过了一些极端情况。

我使用的是 3.10 版的 PyYAML,但看起来有问题的代码与最新版 (3.12) 的 PyYAML 中的代码相同。

【问题讨论】:

【参考方案1】:

在 YAML 中,您可以使用 anchors and aliases。有了它,您可以直接或间接地创建自引用结构。

如果 YAML 没有这种自引用的可能性,您可以先构造所有子结构,然后一次性创建父结构。但是由于自我引用,您可能还没有孩子来“填写”您正在创建的结构。通过使用生成器的两步过程(我称之为两步,因为在方法结束之前它只有一个yield),您可以部分创建一个对象并使用自引用填充它,因为对象存在(即它在内存中的位置已定义)。

好处不在于速度,而纯粹是因为使自引用成为可能。

如果您从您引用的答案中简化示例,则会加载以下内容:

import sys
import ruamel.yaml as yaml


class Foo(object):
    def __init__(self, s, l=None, d=None):
        self.s = s
        self.l1, self.l2 = l
        self.d = d


def foo_constructor(loader, node):
    instance = Foo.__new__(Foo)
    yield instance
    state = loader.construct_mapping(node, deep=True)
    instance.__init__(**state)

yaml.add_constructor(u'!Foo', foo_constructor)

x = yaml.load('''
&fooref
!Foo
s: *fooref
l: [1, 2]
d: try: this
''', Loader=yaml.Loader)

yaml.dump(x, sys.stdout)

但如果您将foo_constructor() 更改为:

def foo_constructor(loader, node):
    instance = Foo.__new__(Foo)
    state = loader.construct_mapping(node, deep=True)
    instance.__init__(**state)
    return instance

(去掉yield,添加最后的return),你会得到一个ConstructorError: with as message

found unconstructable recursive node 
  in "<unicode string>", line 2, column 1:
    &fooref

PyYAML 应该给出类似的消息。检查该错误的回溯,您可以看到 ruamel.yaml/PyYAML 尝试在源代码中解析别名的位置。

【讨论】:

谢谢,我认为这可能与别名和锚点有关。为什么当我按照问题中的描述从your answer 修改foo_constructor 时,我似乎看到了正确的输出?该答案在其示例中具有自我引用。您能否在答案中包含一个示例 YAML 文档,如果我将 foo_constructor 编辑为 not 成为生成器,如我的问题所示? @Ryan 我用 ruamel.yaml 的代码更新了我的答案。 PyYAML 在这方面的行为应该相同。由于它缺乏对 cme​​ts 的跟踪,它的 BaseConstructor.construct_mapping() 代码实际上可能比 ruamel.yaml 的代码更容易理解。 顺便说一句,欢迎来到Stack Overflow,请多发些这么好的问题。 谢谢。你的例子很有帮助。我现在看到了您给出的对象引用自身的示例与我尝试的示例之间的区别。我试图使用您在your other answer 中给出的第三个示例来理解自我引用,但这并不是真正的自我引用。在调试器中浏览这两个示例有助于我理解它。并感谢您欢迎我!仅供参考,我确实验证了您的示例也适用于 PyYAML。 作为背景知识,我开始这项调查的原因是希望保持 YAML 映射中的顺序。 This answer on another post 在我的测试中没有正确处理自引用。我对那个答案和this much more involved answer 之间的差异感到困惑,我想找到一个功能差异,我现在有了,再次感谢。

以上是关于为啥 PyYAML 使用生成器来构造对象?的主要内容,如果未能解决你的问题,请参考以下文章

向使用 PyYaml 生成的 YAML 添加注释

有没有办法在所有节点完成加载后使用 PyYAML 构造映射构造对象?

pyyaml “有序”解析/生成yaml

如何在自定义 PyYAML 构造函数中处理递归?

反射生成对象,调用对象方法

如何在自定义PyYAML构造函数中处理递归?