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

sci*_*tor 16 python yaml pyyaml

我试图在python中创建一个yaml序列,创建一个自定义的python对象.该对象需要使用之后解构的dicts和列表构建__init__.但是,似乎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])'
Run Code Online (Sandbox Code Playgroud)

这工作得很好,因为f持有的引用ld对象,这实际上是充满了数据之后Foo对象被创建.

现在,让我们做一些更复杂的smidgen:

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
Run Code Online (Sandbox Code Playgroud)

现在我们得到以下错误

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
Run Code Online (Sandbox Code Playgroud)

这是因为yaml构造函数在所有节点完成之前在嵌套的外层开始并构造对象之前.有没有办法颠倒顺序并首先从深嵌入(例如嵌套)对象开始?或者,有没有办法让构造至少加载节点的对象之后发生?

sci*_*tor 27

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

装载机类文档清楚地示出的construct_mapping方法只需要在一个单一的参数(node).但是,在考虑编写自己的构造函数之后,我检查了源代码,答案就在那里!该方法还接受一个参数deep(默认为False).

def construct_mapping(self, node, deep=False):
    #...
Run Code Online (Sandbox Code Playgroud)

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

def foo_constructor(loader, node):
    values = loader.construct_mapping(node, deep=True)
    #...
Run Code Online (Sandbox Code Playgroud)

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

  • **谢谢!**这为我省去了一些拉头发. (2认同)
  • 我的天啊!最后 !我会因为这篇文章而吻你:-) (2认同)

Ant*_*hon 9

tl; dr:将此
替换foo_constructor为此答案底部代码中的那个


您的代码(以及您的解决方案)存在一些问题,让我们一步一步地解决它们.

您提供的代码不会打印底线注释中所说的内容,('Foo(1, {'try': 'this'}, [1, 2])')因为没有__str__()定义Foo,它会打印如下内容:

__main__.Foo object at 0x7fa9e78ce850
Run Code Online (Sandbox Code Playgroud)

通过添加以下方法可以轻松解决此问题Foo:

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

然后,如果你看看输出:

Foo(1, [1, 2], {'try': 'this'})
Run Code Online (Sandbox Code Playgroud)

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

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

print(f)
Run Code Online (Sandbox Code Playgroud)

可以预期打印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)
Run Code Online (Sandbox Code Playgroud)

保持这种更新当然是维护的噩梦.

所以废弃整个foo_constructor并用一些看起来更像PyYAML在内部执行此操作的内容替换它:

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

这会处理缺少的(默认)参数,如果关键字参数的默认值发生更改,则不必更新.

所有这些都在一个完整的例子中,包括对象的自引用(总是很棘手):

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'])
Run Code Online (Sandbox Code Playgroud)

得到:

Foo(1, {'try': 'this'}, [1, 2])
Foo(1, None, [1, 2])
Foo({'a': <__main__.Foo object at 0xba9876543210>}, {'try': 'this'}, [1, 2])
Run Code Online (Sandbox Code Playgroud)

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