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

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

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)

И я понимаю, как генерируются данные в 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.


person Ryan    schedule 27.01.2017    source источник


Ответы (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: с сообщением as

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

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

person Anthon    schedule 27.01.2017
comment
Спасибо, я думал, что это может иметь какое-то отношение к псевдонимам и якорям. Почему, когда я изменяю foo_constructor из вашего ответа, как описано в моем вопросе, я вижу правильный вывод? Этот ответ имеет ссылки на себя в своих примерах. Можете ли вы включить в свой ответ пример документа YAML, в котором возникнут проблемы, если я отредактирую foo_constructor так, чтобы он не был генератором, как показано в моем вопросе? - person Ryan; 27.01.2017
comment
@Ryan Я обновил свой ответ, указав код для ruamel.yaml. В этом отношении PyYAML должен вести себя так же. Из-за того, что он не отслеживает комментарии, его код для BaseConstructor.construct_mapping() на самом деле может быть проще для понимания, чем код ruamel.yaml. - person Anthon; 27.01.2017
comment
Кстати, добро пожаловать в Stack Overflow, и, пожалуйста, публикуйте больше таких отличных вопросов. - person Anthon; 27.01.2017
comment
Спасибо. Ваш пример очень полезен. Теперь я вижу разницу между приведенным вами примером, в котором объект ссылается сам на себя, и примером, который я пробовал. Я пытался понять самоссылку, используя третий пример, который вы приводите в вашем другом ответе, но это не совсем так. ссылка на себя, как этот. Просмотр этих двух примеров в отладчике помог мне понять это. И спасибо, что приняли меня! К вашему сведению, я убедился, что ваш пример работает так же и с PyYAML. - person Ryan; 27.01.2017
comment
Еще немного предыстории: причиной, по которой я начал это исследование, было желание сохранить порядок в отображениях YAML. Этот ответ в другом сообщении неправильно обрабатывает ссылку на себя в моем тестировании. Меня смутили различия между этим ответом и этот гораздо более сложный ответ, и я хотел найти функциональную разницу, которая Теперь у меня есть, так что еще раз спасибо. - person Ryan; 27.01.2017
comment
@ryan, вы пробовали это с моим ответом? ruamel.yaml при использовании загрузчика туда и обратно сохраняет порядок ключей в сопоставлении (путем автоматического сохранения вещей в упорядоченном словаре) - person Anthon; 27.01.2017
comment
Кроме того, второй ответ, упомянутый в моем предыдущем комментарии, похоже, неправильно обрабатывает tag:yaml.org,2002:omap. Я думаю, что следует просто опустить строку, которая добавляет конструктор для tag:yaml.org,2002:omap, но у меня недостаточно репутации, чтобы оставить комментарий на этот счет (что побудило меня создать учетную запись). - person Ryan; 27.01.2017
comment
да, я пробовал ruamel.yaml. К сожалению, мы использовали решение из ответа Эрика в нашей кодовой базе, которая в значительной степени зависит от YAML в течение достаточно долгого времени, и Я не уверен, что в настоящее время мы можем пойти на риск перехода на ruamel.yaml. Я также не уверен, что мы хотим сохранить комментарии. Есть ли способ сохранить только упорядочение с ruamel.yaml (а не комментарии)? Еще одним препятствием для использования ruamel.yaml является то, что мы пытаемся использовать CSafeLoader (для повышения производительности), и это не похоже на загрузку туда и обратно. - person Ryan; 27.01.2017
comment
@Ryan Вы должны иметь возможность комбинировать CSafeLoader с RoundTripRepresenter, возможно, с небольшой настройкой. CSafeLoader все равно удаляет комментарии. Но если риск должен оставаться низким, я бы просто перегрузил конструктор сопоставления для PyYAML тем, который сохраняет порядок. - person Anthon; 27.01.2017