在Python中,如何将YAML映射加载为OrderedDict?

151 投票
8 回答
88824 浏览
提问于 2025-04-16 12:33

我想让PyYAML的加载器把映射(还有有序映射)加载到Python 2.7+的OrderedDict类型中,而不是现在使用的普通dict和成对的列表。

这样做的最佳方法是什么呢?

8 个回答

61

oyaml 是一个可以直接替代 PyYAML 的工具,它可以保持字典的顺序。它支持 Python 2 和 Python 3。只需要运行 pip install oyaml,然后按照下面的方式导入:

import oyaml as yaml

这样你在保存或加载数据时,就不会再因为顺序混乱而烦恼了。

注意:我是 oyaml 的作者。

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

撰写回答