加载 YAML 保留顺序
Load YAML preserving order
我有一个 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 文件,因为某些键没有值。所以我有两个问题:
- 如何定义空任务?
- 如何在 Python 中加载文件时保持顺序?
如果其中 none 个可行,是否有其他解决方案?
首先是一个简短的评论:您使用的是列表,而不是数组。此外,您在此数组中使用了元组。
无论如何,你可以为此使用 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
在 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
我可能在字里行间读到,但我假设您的字符串 'task_a'
、'task_b'
等每个都会导致特定类型 (class) 的对象被创建。您可以使用 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
实际上是 位置 参数,您可以使用:
- !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
我有一个 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 文件,因为某些键没有值。所以我有两个问题:
- 如何定义空任务?
- 如何在 Python 中加载文件时保持顺序?
如果其中 none 个可行,是否有其他解决方案?
首先是一个简短的评论:您使用的是列表,而不是数组。此外,您在此数组中使用了元组。
无论如何,你可以为此使用 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
在 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
我可能在字里行间读到,但我假设您的字符串 'task_a'
、'task_b'
等每个都会导致特定类型 (class) 的对象被创建。您可以使用 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
实际上是 位置 参数,您可以使用:
- !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