在Python中,如何将YAML映射加载为OrderedDict?
我想让PyYAML的加载器把映射(还有有序映射)加载到Python 2.7+的OrderedDict类型中,而不是现在使用的普通dict
和成对的列表。
这样做的最佳方法是什么呢?
8 个回答
189
Python >= 3.6
在 Python 3.6 及以上版本中,字典的加载顺序默认是保留的,不需要使用特殊的字典类型。而默认的 Dumper 则是按照键来排序字典。从 pyyaml 5.1
开始,你可以通过传入 sort_keys=False
来关闭这个功能:
a = dict(zip("unsorted", "unsorted"))
s = yaml.safe_dump(a, sort_keys=False)
b = yaml.safe_load(s)
assert list(a.keys()) == list(b.keys()) # True
这个功能之所以能实现,是因为 新的字典实现,这个实现已经在 pypy 中使用了一段时间。虽然在 CPython 3.6 中仍然被视为实现细节,但从 3.7 版本开始,"字典保留插入顺序的特性已经被正式纳入 Python 语言规范",具体可以查看 Python 3.7 新特性。
需要注意的是,这个功能在 PyYAML 的文档中仍然没有说明,所以在安全性要求高的应用中不应该依赖这个功能。
原始答案(兼容所有已知版本)
我喜欢 @James 的 解决方案,因为它简单。不过,它会改变默认的全局 yaml.Loader
类,这可能会导致一些麻烦的副作用。特别是在编写库代码时,这样做是个坏主意。而且,它并不直接适用于 yaml.safe_load()
。
幸运的是,这个解决方案可以在不费太多力气的情况下得到改进:
import yaml
from collections import OrderedDict
def ordered_load(stream, Loader=yaml.SafeLoader, object_pairs_hook=OrderedDict):
class OrderedLoader(Loader):
pass
def construct_mapping(loader, node):
loader.flatten_mapping(node)
return object_pairs_hook(loader.construct_pairs(node))
OrderedLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
construct_mapping)
return yaml.load(stream, OrderedLoader)
# usage example:
ordered_load(stream, yaml.SafeLoader)
对于序列化,你可以使用以下函数:
def ordered_dump(data, stream=None, Dumper=yaml.SafeDumper, **kwds):
class OrderedDumper(Dumper):
pass
def _dict_representer(dumper, data):
return dumper.represent_mapping(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
data.items())
OrderedDumper.add_representer(OrderedDict, _dict_representer)
return yaml.dump(data, stream, OrderedDumper, **kwds)
# usage:
ordered_dump(data, Dumper=yaml.SafeDumper)
在每种情况下,你也可以将自定义的子类设为全局,这样就不需要在每次调用时重新创建它们。
15
注意: 有一个库是基于以下的回答,它还实现了CLoader和CDumpers: Phynix/yamlloader
我非常怀疑这是否是最好的方法,但这是我想到的办法,而且确实有效。也可以在这里找到 作为一个gist。
import yaml
import yaml.constructor
try:
# included in standard lib from Python 2.7
from collections import OrderedDict
except ImportError:
# try importing the backported drop-in replacement
# it's available on PyPI
from ordereddict import OrderedDict
class OrderedDictYAMLLoader(yaml.Loader):
"""
A YAML loader that loads mappings into ordered dictionaries.
"""
def __init__(self, *args, **kwargs):
yaml.Loader.__init__(self, *args, **kwargs)
self.add_constructor(u'tag:yaml.org,2002:map', type(self).construct_yaml_map)
self.add_constructor(u'tag:yaml.org,2002:omap', type(self).construct_yaml_map)
def construct_yaml_map(self, node):
data = OrderedDict()
yield data
value = self.construct_mapping(node)
data.update(value)
def construct_mapping(self, node, deep=False):
if isinstance(node, yaml.MappingNode):
self.flatten_mapping(node)
else:
raise yaml.constructor.ConstructorError(None, None,
'expected a mapping node, but found %s' % node.id, node.start_mark)
mapping = OrderedDict()
for key_node, value_node in node.value:
key = self.construct_object(key_node, deep=deep)
try:
hash(key)
except TypeError, exc:
raise yaml.constructor.ConstructorError('while constructing a mapping',
node.start_mark, 'found unacceptable key (%s)' % exc, key_node.start_mark)
value = self.construct_object(value_node, deep=deep)
mapping[key] = value
return mapping