如何在 yaml (PyYAML) 中隐式标记节点

How to tag nodes implicitly in yaml (PyYAML)

考虑这个 yaml 文件:

!my-type
name: My type
items:
  - name: First item
    number: 42
  - name: Second item
    number: 43

有一个包含一组字典的顶级对象,我可以使用 PyYAML 很好地加载它。现在,我想使用适当的 class 而不是这些项目词典:

!my-type
name: My type
items:
  - !my-type-item
    name: First item
    number: 42
  - !my-type-item
    name: Second item
    number: 43

但是这种语法很麻烦而且多余,因为这个集合中的所有项目都是同一类型。当有数百个这样的项目时,它会变得非常难看。是否可以隐式标记这些项目?

我考虑过使用 yaml.add_path_resolver 但这个 API does not seem 是 public 或稳定的。

YAML 规范说

Resolving the tag of a node must only depend on the following three parameters: (1) the non-specific tag of the node, (2) the path leading from the root to the node and (3) the content (and hence the kind) of the node.

这意味着您在执行此操作时符合规范。我想这就是 add_path_resolver 试图实现的。

这里的问题是 Python 没有 class 带有已声明的类型字段。具有这些语言的语言可以检查它们并隐式加载具有正确类型的数据(由 SnakeYAML、go-yaml 等人完成)。使用 PyYAML,要做到这一点,您需要实现自定义构造函数,例如:

import yaml

def get_value(node, name):
    assert isinstance(node, yaml.MappingNode)
    for key, value in node.value:
        assert isinstance(key, yaml.ScalarNode)
        if key.value == name:
            return value

class MyTypeItem:
    def __init__(self, name, number):
        self.name, self.number = name, number

    @classmethod
    def from_yaml(cls, loader, node):
        name = get_value(node, "name")
        assert isinstance(name, yaml.ScalarNode)

        number = get_value(node, "number")
        assert isinstance(number, yaml.ScalarNode)

        return MyTypeItem(name.value, int(number.value))

    def __repr__(self):
        return f"MyTypeItem(name={self.name}, number={self.number})"

class MyType(yaml.YAMLObject):
    yaml_tag = "!my-type"

    def __init__(self, name, items):
        self.name, self.items = name, items

    @classmethod
    def from_yaml(cls, loader, node):
        name = get_value(node, "name")
        assert isinstance(name, yaml.ScalarNode)

        items = get_value(node, "items")
        assert isinstance(items, yaml.SequenceNode)

        return MyType(name.value,
                [MyTypeItem.from_yaml(loader, n) for n in items.value])

    def __repr__(self):
        return f"MyType(name={self.name}, items={self.items})"

input = """
!my-type
name: My type
items:
  - name: First item
    number: 42
  - name: Second item
    number: 43
"""

print(yaml.load(input, yaml.FullLoader))

这给你:

MyType(name=My type, items=[MyTypeItem(name=First item, number=42), MyTypeItem(name=Second item, number=43)])

只有最上面的 class 派生自 yaml.YAMLObject 并且有一个 yaml_tag,因此 PyYAML 可以隐式地将它用于根项。 MyTypeItem.from_yaml 是从 MyType 明确调用的,因此不需要注册 PyYAML(你 可以 这样做也能够加载包含这样的文件项目直接)。

您需要手动转换为 non-string 值(如 int(number.value) 所示),因为任何标量节点的 .value 始终是字符串。

为了让您自己更轻松,我建议使用 dataclasses 以及 dataclass-wizard 作为高级方法。

这是一种使用 YAMLWizardPyYAML 库将 YAML 解析为嵌套数据类结构的方法:

from __future__ import annotations

from dataclasses import dataclass
from dataclass_wizard import YAMLWizard


@dataclass
class MyContainer(YAMLWizard):
    name: str
    items: list[MyItem]


@dataclass
class MyItem:
    name: str
    number: int


if __name__ == '__main__':
    yaml = """
    name: My type
    items:
      - name: First item
        number: 42
      - name: Second item
        number: 43
    """

    c = MyContainer.from_yaml(yaml)
    print(c)

输出:

MyContainer(name='My type', items=[MyItem(name='First item', number=42), MyItem(name='Second item', number=43)])

注意:这需要额外的yaml,然后引入PyYAML依赖:

$ pip install dataclass-wizard[yaml]