19

我正在尝试在 python 中创建一个创建自定义 python 对象的 yaml 序列。该对象需要使用在__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 <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 构造函数是在嵌套之前的外层开始,在所有节点完成之前构造对象。有没有办法颠倒顺序并首先从深度嵌入(例如嵌套)对象开始?或者,有没有办法至少加载节点的对象之后进行构造?

4

3 回答 3

32

好吧,你知道什么。我找到的解决方案非常简单,但文档却不是那么好。

Loader 类文档清楚地表明该方法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 可以使用一些额外的文档,但我很感激它已经存在。

于 2013-10-18T06:17:27.243 回答
11

tl;博士:用这个答案底部的代码
替换你的foo_constructor


您的代码(和您的解决方案)存在几个问题,让我们逐步解决它们。

您提供的代码不会打印它在底线注释中所说的内容, ( 'Foo(1, {'try': 'this'}, [1, 2])'​​) 因为没有__str__()定义 for 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'})

这很接近,但也不是您在评论中承诺的。Thelist和 thedict被交换,因为在foo_constructor()你创建Foo()的参数顺序错误。
这指出了一个更基本的问题,您foo_constructor() 需要对它正在创建的对象有更多了解。为什么会这样?不只是参数顺序,试试:

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

print(f)

人们会期望这会打印(使用未指定关键字参数Foo(1, None, [1, 2])的默认值)。 你得到的是一个 KeyError 异常。d
d = value['d']

您可以使用get('d')foo_constructor()来解决此问题,但您必须意识到,为了正确的行为,您必须为每个具有默认值的参数指定默认值Foo.__init__()(在您的情况下恰好是 all )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 本身起作用。

于 2016-02-18T09:00:34.100 回答
2

除了您自己的答案,scicalculator:如果您希望下次不必记住这个标志,和/或希望有一个更面向对象的方法,您可以使用yamlable,我写它是为了简化 yaml-to-我们的生产代码的对象绑定。

这就是您编写示例的方式:

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以便在两个方向上自定义映射。

于 2018-07-11T12:01:01.390 回答