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

时间:2013-10-18 00:47:58

标签: python yaml pyyaml

我试图在python中创建一个yaml序列来创建一个自定义的python对象。需要使用在__init__之后解构的dicts和列表构造对象。但是,似乎construct_mapping函数不构造嵌入序列(列表)和dicts的整个树 请考虑以下事项:

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包含对ld对象的引用,这些对象实际上填充了数据 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 <module>
    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构造函数在所有节点完成之前在嵌套的外层开始并构造对象之前。有没有办法颠倒顺序并首先从深度嵌入(例如嵌套)对象开始?或者,有没有办法在节点的对象加载后至少发生构造?

3 个答案:

答案 0 :(得分:25)

嗯,你知道些什么。我发现的解决方案非常简单,但没有那么好记录。

Loader class documentation清楚地显示construct_mapping方法只接受一个参数(node)。但是,在考虑编写自己的构造函数之后,我检查了源代码,答案是right there!该方法还接受参数deep(默认为False)。

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

因此,正确使用的构造函数方法是

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

我想PyYaml可以使用一些额外的文档,但我很感激它已经存在。

答案 1 :(得分:8)

  

TL; DR:
  将您的foo_constructor替换为此答案底部代码中的{1}}

您的代码(以及您的解决方案)存在一些问题,请逐步解决。

您提供的代码不会打印底线评论('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'})

这很接近,但不是你在评论中所承诺的。 listdict被交换,因为在foo_constructor()中,您使用错误的参数顺序创建Foo()
这指出了一个更基本的问题,即foo_constructor() 需要了解它正在创建的对象。为什么会这样?它不仅仅是参数顺序,请尝试:

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

print(f)

可以预期这会打印Foo(1, None, [1, 2])(使用未指定的d关键字参数的默认值。)
得到的是d = value['d']上的KeyError异常。

您可以在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': <__main__.Foo object at 0xba9876543210>}, {'try': 'this'}, [1, 2])

这是使用ruamel.yaml(我是作者)测试的,这是PyYAML的增强版本。对于PyYAML本身,该解决方案应该是相同的。

答案 2 :(得分:1)

your own answer之外,还有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_dictfrom_yaml_dict,以便在两个方向上自定义映射。