【问题标题】:Load YAML preserving order加载 YAML 保留顺序
【发布时间】:2017-05-17 20:48:13
【问题描述】:

我有一个 Python 库,它定义了一个类似下一个的列表:

config = [
    'task_a',
    ('task_b', {'task_b_opt_1': ' '}),
    ('task_a', {'task_a_opt_1': 42}),
    ('task_c', {'task_c_opt_1': 'foo', 'task_c_opt_2': 'bar'}),
    'task_b'
]

基本上,此列表定义了 5 个任务,这些任务必须按特定顺序应用,并使用定义的参数(如果有)。此外,同一任务可以定义参数或不定义参数(使用默认值)。

现在我想扩展库以支持配置文件。为了让最终用户更容易使用 YAML 文件,我正在考虑使用它们。所以上面的代码会变成这样:

task_a:
task_b:
  task_b_opt_1: ' '
task_a:
  task_a_opt_1: 42
task_c:
  task_c_opt_1': 'foo'
  task_c_opt_2': 'bar'
task_b:

这甚至不是一个有效的 YAML 文件,因为某些键没有价值。所以我有两个问题:

  1. 如何定义空任务?
  2. 如何在 Python 中加载文件时保持顺序?

如果这些都不可行,还有其他解决方案吗?

【问题讨论】:

  • 一个key后面没有值其实是有效的,无效的是同一个mapping中多次拥有同一个key。
  • 您的某些键上还有右引号,而没有相应的左引号。键需要完全引用 ('task_c_opt_1') 或完全不引用 (task_c_opt_1)。

标签: python yaml configuration-files


【解决方案1】:

在 YAML 中,映射被定义为无序。典型的解决方案是使其成为一个映射列表。但是,值(甚至键)可能会丢失,在这种情况下,它们隐含为 null(相当于 Python 中的 None

- task_a:
- task_b:
    task_b_opt_1: ' '
- task_a:
    task_a_opt_1: 42
- task_c:
    task_c_opt_1: 'foo'
    task_c_opt_2: 'bar'
- task_b:

另一种选择是不将没有选项的任务转换为映射,而是使用字符串,只需从这些行中删除 :

- task_a
- task_b:
    task_b_opt_1: ' '
- task_a:
    task_a_opt_1: 42
- task_c:
    task_c_opt_1: 'foo'
    task_c_opt_2: 'bar'
- task_b

【讨论】:

  • 我要评论说指定'null'不是必需的,但你已经改变了。但是,您还应该删除注释“null 将在 Python 中转换为 None”,因为现在 YAML 中没有明确的 null 标量,这不再有意义。源代码中有一些多余的单引号,并且'foo' 和 'bar' 周围的引号并不是必须的。
  • @Anthon 啊,谢谢。我删除了那行,并在那之后修复了我提到的: null。我保留了值的引号,因为它们在原始问题中,但是通过修复键的半引号,我可以添加回语法突出显示。
【解决方案2】:

首先快速评论:您使用的是列表,而不是数组。此外,您正在此数组中使用元组。

无论如何,您可以为此使用 yaml 模块,我还将元组更改为列表,因为 yaml 中没有元组。

from yaml import dump

config = [
    'task_a',
    ['task_b', {'task_b_opt_1': ' '}],
    ['task_a', {'task_a_opt_1': 42}],
    ['task_c', {'task_c_opt_1': 'foo', 'task_c_opt_2': 'bar'}],
    'task_b'
]
print dump(config)

打印出来:

- task_a
- - task_b
  - {task_b_opt_1: ' '}
- - task_a
  - {task_a_opt_1: 42}
- - task_c
  - {task_c_opt_1: foo, task_c_opt_2: bar}
- task_b

【讨论】:

  • 如果您只是要打印构建为字符串的结果,那么不向dump 提供流参数是低效的。执行dump(config, sys.stdout) 的内存效率更高、速度更快。而print as 语句无论如何在 Python3 中都不起作用。
  • @Anthon:我把打印放在那里是为了在 StackOverflow 中显示结果。此外,他说他会将其用于配置文件。我敢肯定,在这种情况下,您不会达到流媒体带来显着差异的大小。
【解决方案3】:

我可能在字里行间阅读,但我假设您的字符串'task_a''task_b' 等都会导致创建特定类型(类)的对象。您可以使用 YAML 标签直接指定这些对象类型,从而生成以下 YAML 文档:

- !task_a
- !task_b
  task_b_opt_1: ' '
- !task_a
  task_a_opt_1: 42
- !task_c
  task_c_opt_1: foo
  task_c_opt_2: bar
- !task_b

如果您的 task_X_opt_N 实际上是 positional 参数,您可以使用:

- !task_a
- !task_b
  - ' '
- !task_a
  - 42
- !task_c
  - foo
  - bar
- !task_b

IMO 更具可读性(最终用户编辑这些内容时更不容易出错)。

这些格式中的任何一种都可以通过以下方式加载:

import ruamel.yaml

class Task:
    def __init__(self, *args, **kw):
        if args: assert len(kw) == 0
        if kw: assert len(args) == 0
        self.args = args
        self.opt = kw

    def __repr__(self):
        retval = str(self.__class__.__name__)
        task_letter = retval[-1].lower()
        for idx, k in enumerate(self.args):
            retval += '\n  task_{}_opt_{}: {!r}'.format(task_letter, idx, k)
        for k in sorted(self.opt):
            retval += '\n  {}: {!r}'.format(k, self.opt[k])
        return retval

class TaskA(Task):
    pass


class TaskB(Task):
    pass


class TaskC(Task):
    pass



def default_constructor(loader, tag_suffix, node):
    assert tag_suffix.startswith('!task_')
    if tag_suffix[6] == 'a':
        task = TaskA
    elif tag_suffix[6] == 'b':
        task = TaskB
    elif tag_suffix[6] == 'c':
        task = TaskC
    else:
        raise NotImplementedError('Unknown task type' + tag_suffix)
    if isinstance(node, ruamel.yaml.ScalarNode):
        assert node.value == ''
        return task()
    elif isinstance(node, ruamel.yaml.MappingNode):
        val = loader.construct_mapping(node)
        return task(**val)
    elif isinstance(node, ruamel.yaml.SequenceNode):
        val = loader.construct_sequence(node)
        return task(*val)
    else:
        raise NotImplementedError('Node: ' + str(type(node)))

ruamel.yaml.add_multi_constructor('', default_constructor,
                                  constructor=ruamel.yaml.SafeConstructor)


with open('config.yaml') as fp:
    tasks = ruamel.yaml.safe_load(fp)
    for task in tasks:
        print(task)

导致相同的输出:

TaskA
TaskB
  task_b_opt_1: ' '
TaskA
  task_a_opt_1: 42
TaskC
  task_c_opt_1: 'foo'
  task_c_opt_2: 'bar'
TaskB

如果由于某种原因您需要使用旧的 PyYAML,您可以导入它并使用以下方法添加构造函数:

ruamel.yaml.add_multi_constructor('', default_constructor,
                                  Loader=yaml.SafeLoader)

您必须注意 PyYAML 仅支持 YAML 1.1 而不是 YAML 1.2

【讨论】:

  • 如果任务类型很多,最好扫描模块中的TaskX 类并自动将它们添加到映射中。
猜你喜欢
  • 1970-01-01
  • 2020-10-22
  • 2023-04-05
  • 2018-05-03
  • 2016-02-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多