如何在 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
作为高级方法。
这是一种使用 YAMLWizard
和 PyYAML
库将 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]
考虑这个 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
作为高级方法。
这是一种使用 YAMLWizard
和 PyYAML
库将 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]