Рассмотрим этот файл yaml:
!my-type
name: My type
items:
- name: First item
number: 42
- name: Second item
number: 43
Есть один объект верхнего уровня, содержащий набор словарей, и я могу нормально загрузить его с помощью PyYAML. Теперь я хочу использовать правильный класс вместо этих словарей элементов:
!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 не похоже должен быть общедоступным или стабильным.
Спецификация 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 нет классов с объявленными типизированными полями. Языки, в которых они есть, могут проверять их и неявно загружать данные соответствующего типа (сделано 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)])
Только самый верхний класс является производным от yaml.YAMLObject
и имеет yaml_tag
, так что PyYAML может неявно использовать его для корневого элемента. MyTypeItem.from_yaml
вызывается явно из MyType
и, следовательно, не требует регистрации в PyYAML (вы, могу, делаете это, чтобы иметь возможность напрямую загружать файлы, содержащие такой элемент).
Вам необходимо выполнить преобразование в нестроковые значения вручную (как показано с помощью int(number.value)
), поскольку .value
любого скалярного узла всегда является строкой.