我试图在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
包含对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构造函数在所有节点完成之前在嵌套的外层开始并构造对象之前。有没有办法颠倒顺序并首先从深度嵌入(例如嵌套)对象开始?或者,有没有办法在节点的对象加载后至少发生构造?
答案 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'})
这很接近,但不是你在评论中所承诺的。 list
和dict
被交换,因为在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_dict
和from_yaml_dict
,以便在两个方向上自定义映射。