Есть ли способ построить объект, используя PyYAML construct_mapping после завершения загрузки всех узлов?

Я пытаюсь создать последовательность yaml в python, которая создает пользовательский объект python. Объект должен быть построен с помощью диктов и списков, которые деконструируются после__init__, Однако, похоже, что функция construct_mapping не создает полное дерево встроенных последовательностей (списков) и диктовок.

Учтите следующее:

import yaml

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

def foo_constructor(loader, node):
    values = loader.construct_mapping(node)
    s = values["s"]
    d = values["d"]
    l = values["l"]
    return Foo(s, d, l)
yaml.add_constructor(u'!Foo', foo_constructor)

f = yaml.load('''
--- !Foo
s: 1
l: [1, 2]
d: {try: this}''')

print(f)
# prints: 'Foo(1, {'try': 'this'}, [1, 2])'

Это работает нормально, потому чтоf содержит ссылки наl а такжеd объекты, которые фактически заполнены даннымипосле Foo объект создан.

Теперь давайтесделать что-то более сложное:

class Foo(object):
    def __init__(self, s, l=None, d=None):
        self.s = s
        # assume two-value list for l
        self.l1, self.l2 = l
        self.d = d

Теперь мы получаем следующую ошибку

Traceback (most recent call last):
  File "test.py", line 27, in 
    d: {try: this}''')
  File "/opt/homebrew/lib/python2.7/site-packages/yaml/__init__.py", line 71, in load
    return loader.get_single_data()
  File "/opt/homebrew/lib/python2.7/site-packages/yaml/constructor.py", line 39, in get_single_data
    return self.construct_document(node)
  File "/opt/homebrew/lib/python2.7/site-packages/yaml/constructor.py", line 43, in construct_document
    data = self.construct_object(node)
  File "/opt/homebrew/lib/python2.7/site-packages/yaml/constructor.py", line 88, in construct_object
    data = constructor(self, node)
  File "test.py", line 19, in foo_constructor
    return Foo(s, d, l)
  File "test.py", line 7, in __init__
    self.l1, self.l2 = l
ValueError: need more than 0 values to unpack

Это связано с тем, что конструктор yaml запускается на внешнем слое, перед тем как вложить все объекты, до того как все узлы будут завершены. Есть ли способ изменить порядок и начать сначала с глубоко внедренных (например, вложенных) объектов? В качестве альтернативы, есть ли способ заставить строительство по крайней мерепосле узел "s объекты были загружены?

Ответы на вопрос(3)

ТЛ; др:

заменить свойfoo_constructor с одним в коде в нижней части этого ответа

Есть несколько проблем с вашим кодом (и вашим решением), давайтеОбращайтесь к ним шаг за шагом.

Код, который вы предоставляете, не будет печатать то, что написано в комментарии в нижней строке, ('Foo(1, {'try': 'this'}, [1, 2])') как нет__str__() определено дляFoo, он печатает что-то вроде:

__main__.Foo object at 0x7fa9e78ce850

Это легко исправить, добавив следующий метод:Foo

    def __str__(self):
        # print scalar, dict and list
        return('Foo({s}, {d}, {l})'.format(**self.__dict__))

и если вы посмотрите на вывод:

Foo(1, [1, 2], {'try': 'this'})

Это близко, но не то, что вы обещали в комментарии.list иdict поменялись местами, потому что в вашемfoo_constructor() вы создаетеFoo() с неправильным порядком параметров.

Это указывает на более фундаментальную проблему,foo_constructor() нужно много знать об объекте, который он создает, Почему это так? Это не просто порядок параметров, попробуйте:

f = yaml.load('''
--- !Foo
s: 1
l: [1, 2]
''')

print(f)

Можно ожидать, что это напечататьFoo(1, None, [1, 2]) (со значением по умолчанию не указаноd ключевой аргумент).

Вы получаете исключение KeyError.d = value['d']

Вы можете использоватьget('d')и т. д., вfoo_constructor() чтобы решить это, но вы должны понимать, что для правильного поведения выдолжен укажите значения по умолчанию из вашегоFoo.__init__() (который в вашем случае просто случаетсяNone), для каждого параметра со значением по умолчанию:

def foo_constructor(loader, node):
    values = loader.construct_mapping(node, deep=True)
    s = values["s"]
    d = values.get("d", None)
    l = values.get("l", None)
    return Foo(s, l, d)

держать это в курсе, конечно, кошмар обслуживания.

Так что ломай весьfoo_constructor и замените его чем-то, что больше похоже на то, как PyYAML делает это внутренне:

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

Это обрабатывает отсутствующие (по умолчанию) параметры и неДолжны быть обновлены, если изменяются значения по умолчанию для ваших аргументов ключевого слова.

Все это в полном примере, включая самообращенное использование объекта (всегда сложно):

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

    def __str__(self):
        # print scalar, dict and list
        return('Foo({s}, {d}, [{l1}, {l2}])'.format(**self.__dict__))

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)

print(yaml.load('''
--- !Foo
s: 1
l: [1, 2]
d: {try: this}'''))
print(yaml.load('''
--- !Foo
s: 1
l: [1, 2]
'''))
print(yaml.load('''
&fooref
a: !Foo
  s: *fooref
  l: [1, 2]
  d: {try: this}
''')['a'])

дает:

Foo(1, {'try': 'this'}, [1, 2])
Foo(1, None, [1, 2])
Foo({'a': }, {'try': 'this'}, [1, 2])

Это было проверено с помощьюruamel.yaml (автором которой я являюсь), которая является расширенной версией PyYAML. Решение должно работать так же для самого PyYAML.

 Anthon18 февр. 2016 г., 10:11
Для тех, кто интересуется кровавыми подробностями о том, почему вам нужно создать (частично) созданный экземпляр, посмотрите наconstructor.py:BaseConstructor.construct_object() там делается тест, чтобы увидеть, зарегистрирован ли конструктор (т.е.foo_constructor()) вернутьGeneratorTypeи соответствующие действия приняты.

В дополнение ктвой собственный ответ, scicalculator: если вы не хотите запоминать этот флаг в следующий раз и / или хотите использовать более объектно-ориентированный подход, вы можете использоватьyamlableЯ написал это, чтобы облегчить привязку yaml к объекту для нашего производственного кода.

Вот как бы вы написали свой пример:

import yaml
from yamlable import YamlAble, yaml_info

@yaml_info(yaml_tag_ns="com.example")
class Foo(YamlAble):
    def __init__(self, s, l=None, d=None):
        self.s = s
        # assume two-value list for l
        self.l1, self.l2 = l
        self.d = d

    def __str__(self):
        return "Foo({s}, {d}, {l})".format(s=self.s, d=self.d, l=[self.l1, self.l2])

    def to_yaml_dict(self):
        """ override because we do not want the default vars(self) """
        return {'s': self.s, 'l': [self.l1, self.l2], 'd': self.d}

    # @classmethod
    # def from_yaml_dict(cls, dct, yaml_tag):
    #     return cls(**dct) 


f = yaml.safe_load('''
--- !yamlable/com.example.Foo
s: 1
l: [1, 2]
d: {try: this}''')

print(f)

доходность

Foo(1, {'try': 'this'}, [1, 2])

и вы можете сбросить тоже:

>>> print(yaml.safe_dump(f))

!yamlable/com.example.Foo
d: {try: this}
l: [1, 2]
s: 1

Обратите внимание, как два методаto_yaml_dict а такжеfrom_yaml_dict можно переопределить, чтобы настроить отображение в обоих направлениях.

Решение Вопроса

Ну, что же вы знаете. Решение, которое я нашел, было таким простым, но не очень хорошо задокументировано.

Документация по классу погрузчика четко показываетconstruct_mapping метод принимает только один параметр (node). Тем не менее, после рассмотрения написания моего собственного конструктора, я проверил источник, и ответ былпрямо там! Метод также принимает параметрdeep (по умолчанию False).

def construct_mapping(self, node, deep=False):
    #...

Итак, правильный метод конструктора для использования

def foo_constructor(loader, node):
    values = loader.construct_mapping(node, deep=True)
    #...

Я думаю, что PyYaml мог бы использовать дополнительную документацию, но яЯ благодарен, что он уже существует.

 scicalculator11 апр. 2018 г., 01:51
@HendrikWiese Это ссылка, которую вы ищете?Новая гиперссылка на Github
 Hendrik Wiese09 апр. 2018 г., 15:55
Если бы только ссылка непрошло 404 ...
 user55454613 мар. 2015 г., 05:52
Спасибо! Это спасло меня от вырванных волос.
 Penbeuz12 янв. 2016 г., 17:12
О, МОЙ БОГ! наконец ! Я бы поцеловал тебя за этот пост :-)
 Dacav13 окт. 2015 г., 13:52
Хотел бы я дать этому парню больше очков.

Ваш ответ на вопрос