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

Why does PyYAML use generators to construct objects?

我一直在阅读 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=False 对应 construct_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 中是相同的。

在 YAML 中你可以有 anchors and aliases。有了它,您可以直接或间接地制作 self-referential 结构。

如果YAML没有这种self-reference的可能性,你可以先构造所有的children,然后一次性创建parent结构。但是由于 self-reference,您可能还没有 child 到 "fill-out" 您正在创建的结构。通过使用生成器的 two-step 过程(我称之为两步,因为在方法结束之前它只有一个产量),您可以创建一个 object 部分并填充它用 self-reference 输出,因为 object 存在(即定义了它在内存中的位置)。

好处不在于速度,而纯粹是因为使 self-reference 成为可能。

如果根据您参考的答案稍微简化示例,将加载以下内容:

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 尝试解析源代码中的别名的位置。