В Python, как вы можете загрузить сопоставления YAML как OrderedDicts?

Я хотел бы получить загрузчик PyYAML для загрузки отображений (и упорядоченных отображений) в тип Python 2.7+ OrderedDict вместо ванили dict и список пар, которые он использует в настоящее время.

Какой лучший способ сделать это?

9 ответов

Решение

Примечание: есть библиотека, основанная на следующем ответе, которая также реализует CLoader и CDumpers: https://github.com/Phynix/yamlloader

Я очень сомневаюсь, что это лучший способ сделать это, но так я придумал, и это работает. Также доступно как суть.

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

Обновление: в Python 3.6+ вы, вероятно, не нуждаетесь OrderedDict вообще из-за новой реализации dict, которая использовалась в Pypy в течение некоторого времени (хотя рассмотрены детали реализации CPython на данный момент).

Обновление: в python 3.7+ природа сохранения порядка вставки объектов dict была объявлена ​​официальной частью спецификации языка Python, см. Что нового в Python 3.7.

Мне нравится решение @James за его простоту. Тем не менее, он меняет глобальный по умолчанию yaml.Loader класс, который может привести к неприятным побочным эффектам. Особенно при написании библиотечного кода это плохая идея. Кроме того, он не работает напрямую с yaml.safe_load(),

К счастью, решение можно улучшить без особых усилий:

import yaml
from collections import OrderedDict

def ordered_load(stream, Loader=yaml.Loader, 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.Dumper, **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)

Опция 2018:

oyaml является заменой PyYAML, которая сохраняет точный порядок. Оба Python 2 и Python 3 поддерживаются. Просто pip install oyaml и импортируйте, как показано ниже:

import oyaml as yaml

Вы больше не будете раздражаться ошибочными отображениями при выгрузке / загрузке.

Примечание: я автор oyaml.

Модуль yaml позволяет вам задавать пользовательские "представители" для преобразования объектов Python в текст и "конструкторы" для обратного процесса.

_mapping_tag = yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG

def dict_representer(dumper, data):
    return dumper.represent_dict(data.iteritems())

def dict_constructor(loader, node):
    return collections.OrderedDict(loader.construct_pairs(node))

yaml.add_representer(collections.OrderedDict, dict_representer)
yaml.add_constructor(_mapping_tag, dict_constructor)

Вариант 2015 (и позже):

ruamel.yaml - это замена для PyYAML (отказ от ответственности: я являюсь автором этого пакета). Сохранение порядка отображений было одной из вещей, добавленных в первой версии (0.1) еще в 2015 году. Оно не только сохраняет порядок ваших словарей, но также сохраняет комментарии, имена якорей, теги и поддерживает YAML 1.2. спецификация (выпущена в 2009 году)

В спецификации говорится, что упорядочение не гарантируется, но, конечно, в YAML-файле есть упорядочение, и соответствующий синтаксический анализатор может просто придерживаться этого и прозрачно генерировать объект, который сохраняет упорядоченность. Вам просто нужно правильно выбрать парсер, загрузчик и дампер¹:

import sys
import ruamel.yaml as yaml

yaml_str = """\
3: abc
conf:
    10: def
    3: gij     # h is missing
more:
- what
- else
"""

data = yaml.load(yaml_str, Loader=yaml.RoundTripLoader)
data['conf'][10] = 'klm'
data['conf'][3] = 'jig'
yaml.dump(data, sys.stdout, Dumper=yaml.RoundTripDumper)

дам тебе:

3: abc
conf:
  10: klm
  3: jig       # h is missing
more:
- what
- else

Данные имеют тип CommentedMap, который функционирует как dict, но имеет дополнительную информацию, которая сохраняется до момента сброса (включая сохраненный комментарий!)

Обновление: библиотека устарела в пользу yamlloader (который основан на yamlordereddictloader)

Я только что нашел библиотеку Python ( https://pypi.python.org/pypi/yamlordereddictloader/0.1.1), которая была создана на основе ответов на этот вопрос и довольно проста в использовании:

import yaml
import yamlordereddictloader

datas = yaml.load(open('myfile.yml'), Loader=yamlordereddictloader.Loader)

В моей установке For PyYaml для Python 2.7 я обновил __init__.py, constructor.py и loader.py. Теперь поддерживает опцию object_pairs_hook для команд загрузки. Разница изменений, которые я сделал, ниже.

__init__.py

$ diff __init__.py Original
64c64
< def load(stream, Loader=Loader, **kwds):
---
> def load(stream, Loader=Loader):
69c69
<     loader = Loader(stream, **kwds)
---
>     loader = Loader(stream)
75c75
< def load_all(stream, Loader=Loader, **kwds):
---
> def load_all(stream, Loader=Loader):
80c80
<     loader = Loader(stream, **kwds)
---
>     loader = Loader(stream)

constructor.py

$ diff constructor.py Original
20,21c20
<     def __init__(self, object_pairs_hook=dict):
<         self.object_pairs_hook = object_pairs_hook
---
>     def __init__(self):
27,29d25
<     def create_object_hook(self):
<         return self.object_pairs_hook()
<
54,55c50,51
<         self.constructed_objects = self.create_object_hook()
<         self.recursive_objects = self.create_object_hook()
---
>         self.constructed_objects = {}
>         self.recursive_objects = {}
129c125
<         mapping = self.create_object_hook()
---
>         mapping = {}
400c396
<         data = self.create_object_hook()
---
>         data = {}
595c591
<             dictitems = self.create_object_hook()
---
>             dictitems = {}
602c598
<             dictitems = value.get('dictitems', self.create_object_hook())
---
>             dictitems = value.get('dictitems', {})

loader.py

$ diff loader.py Original
13c13
<     def __init__(self, stream, **constructKwds):
---
>     def __init__(self, stream):
18c18
<         BaseConstructor.__init__(self, **constructKwds)
---
>         BaseConstructor.__init__(self)
23c23
<     def __init__(self, stream, **constructKwds):
---
>     def __init__(self, stream):
28c28
<         SafeConstructor.__init__(self, **constructKwds)
---
>         SafeConstructor.__init__(self)
33c33
<     def __init__(self, stream, **constructKwds):
---
>     def __init__(self, stream):
38c38
<         Constructor.__init__(self, **constructKwds)
---
>         Constructor.__init__(self)

На эту тему есть билет PyYAML, открытый 5 лет назад. Он содержит некоторые релевантные ссылки, включая ссылку на этот самый вопрос:) Я лично взял gist 317164 и немного изменил его, чтобы использовать OrderedDict из Python 2.7, а не включенную реализацию (просто заменил класс на from collections import OrderedDict).

Вот простое решение, которое также проверяет наличие дублированных ключей верхнего уровня на вашей карте.

import yaml
import re
from collections import OrderedDict

def yaml_load_od(fname):
    "load a yaml file as an OrderedDict"
    # detects any duped keys (fail on this) and preserves order of top level keys
    with open(fname, 'r') as f:
        lines = open(fname, "r").read().splitlines()
        top_keys = []
        duped_keys = []
        for line in lines:
            m = re.search(r'^([A-Za-z0-9_]+) *:', line)
            if m:
                if m.group(1) in top_keys:
                    duped_keys.append(m.group(1))
                else:
                    top_keys.append(m.group(1))
        if duped_keys:
            raise Exception('ERROR: duplicate keys: {}'.format(duped_keys))
    # 2nd pass to set up the OrderedDict
    with open(fname, 'r') as f:
        d_tmp = yaml.load(f)
    return OrderedDict([(key, d_tmp[key]) for key in top_keys])
Другие вопросы по тегам