Почему PyYAML использует генераторы для создания объектов?

Я читал исходный код PyYAML, чтобы попытаться понять, как определить подходящую конструкторскую функцию, которую я могу добавить с помощью add_constructor. У меня довольно хорошее представление о том, как работает этот код, но я до сих пор не понимаю, почему конструкторы YAML по умолчанию в SafeConstructor являются генераторами. Например, метод construct_yaml_map of SafeConstructor:

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

Я понимаю, как генератор используется в BaseConstructor.construct_object следующим образом, чтобы заглушить объект и только заполнить его данными из node, если 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)

И я понимаю, как данные генерируются в BaseConstructor.construct_document в случае, когда deep=False для construct_mapping.

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, или это обеспечивает производительность?

Этот ответ по другому вопросу был несколько полезен, но я не понимаю, почему этот ответ делает это:

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 вы можете иметь якоря и псевдонимы. С этим вы можете прямо или косвенно создавать самореферентные структуры.

Если у YAML не было такой возможности самореференции, вы могли бы сначала построить все дочерние элементы, а затем создать родительскую структуру за один раз. Но из-за самореференций у вас может не быть ребенка, чтобы "заполнить" структуру, которую вы создаете. Используя двухэтапный процесс генератора (я называю этот два шага, потому что он имеет только один доход до того, как вы дойдете до конца метода), вы можете частично создать объект и заполнить его саморекламой, поскольку объект существует (т.е. определено его место в памяти).

Преимущество не в скорости, а исключительно из-за возможности самооценки.

Если вы упростите пример из ответа, вы обратитесь к бит, следующие загрузки:

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

(выход удален, добавлен окончательный возврат), вы получите ConstructorError: с сообщением

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

PyYAML должен дать аналогичное сообщение. Осмотрите трассировку на этой ошибке, и вы увидите, где ruamel.yaml/PyYAML пытается разрешить псевдоним в исходном коде.