【问题标题】:Is there a way to construct an object using PyYAML construct_mapping after all nodes complete loading?有没有办法在所有节点完成加载后使用 PyYAML 构造映射构造对象?
【发布时间】:2013-10-26 17:06:49
【问题描述】:

我正在尝试在 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 包含对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构造函数是在嵌套before的外层开始,在所有节点完成之前构造对象。有没有办法颠倒顺序并首先从深度嵌入(例如嵌套)对象开始?或者,有没有办法让构造至少在节点的对象加载之后发生?

【问题讨论】:

    标签: python yaml pyyaml


    【解决方案1】:

    嗯,你知道什么。我找到的解决方案非常简单,但文档却没有那么完善。

    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 可以使用一些额外的文档,但我很感激它已经存在。

    【讨论】:

    • 谢谢!这为我节省了一些被拔掉的头发。
    • 天哪!最后 !我会为这篇文章亲吻你:-)
    • @HendrikWiese 这是您要查找的链接吗? New Github hyperlink
    【解决方案2】:

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


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

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

    __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 异常。

    您可以在foo_constructor() 中使用get('d') 等来解决此问题,但您必须意识到,为了正确的行为,您必须从您的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 本身起作用。

    【讨论】:

    • 对于那些对为什么需要产生(部分)创建的实例的血腥细节感兴趣的人,请查看constructor.py:BaseConstructor.construct_object() 在那里进行测试以查看注册的构造函数(即foo_constructor() ) 返回GeneratorType,并采取适当的措施。
    【解决方案3】:

    除了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 这两个方法,以便自定义双向映射。

    【讨论】:

      猜你喜欢
      • 2015-10-11
      • 1970-01-01
      • 1970-01-01
      • 2018-04-21
      • 2014-02-14
      • 2012-12-27
      • 1970-01-01
      • 2011-11-01
      • 1970-01-01
      相关资源
      最近更新 更多