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

128

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

Как лучше всего это сделать?

Эрик Нэсет
источник

Ответы:

148

Обновление: в 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)
coldfix
источник
3
+1 - большое спасибо за это, избавил меня от многих хлопот.
Nobilis
2
Эта реализация нарушает теги слияния YAML, BTW
Рэнди,
1
@Randy Спасибо. Раньше я не запускал этот сценарий, но теперь я добавил исправление, чтобы справиться с этим (надеюсь).
coldfix
9
@ArneBabenhauserheide Я не уверен, что PyPI достаточно развит , но взгляните на ruamel.yaml (я автор этого), если вы думаете, что это так.
Anthon
1
@Anthon Ваша библиотека ruamel.yaml работает очень хорошо. Спасибо за это.
Ян Влцинский
56

Модуль 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)
Брайс М. Демпси
источник
5
какие объяснения этому ответу?
Шуман
1
Или даже лучше, from six import iteritemsа затем измените его на iteritems(data)так, чтобы он одинаково хорошо работал в Python 2 и 3.
Midnighter
5
Похоже, здесь используются недокументированные функции PyYAML ( represent_dictи DEFAULT_MAPPING_TAG). Это потому, что документация неполная, или эти функции не поддерживаются и могут быть изменены без предварительного уведомления?
aldel
3
Обратите внимание, что dict_constructorвам нужно позвонить, loader.flatten_mapping(node)иначе вы не сможете загрузить <<: *...(синтаксис слияния)
Энтони Соттиль,
@ brice-m-dempsey вы можете добавить какой-нибудь пример использования вашего кода? В моем случае (Python 3.7) это не работает
schaffe
53

Вариант 2018:

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

import oyaml as yaml

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

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

Wim
источник
1
Спасибо тебе за это! По какой-то причине даже в Python 3.8 порядок не соблюдался в PyYaml. oyaml сразу решил это за меня.
Джон Смит Необязательно
26

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

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

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

import sys
from ruamel.yaml import YAML

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

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

дам тебе:

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

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

Энтон
источник
Это неплохо, если у вас уже есть файл YAML, но как это сделать с помощью структуры Python? Я пробовал использовать CommentedMapнапрямую, но это не работает и OrderedDictставит !!omapвезде, что не очень удобно.
Холт,
Я не уверен, почему CommentedMap не сработал для вас. Можете ли вы разместить вопрос со своим (минимизированным) кодом и пометить его как ruamel.yaml? Тогда я получу уведомление и отвечу.
Антон
Извините, я думаю, это потому, что я пытался сохранить с CommentedMapпомощью safe=Truein YAML, но это не сработало (с использованием safe=Falseработает). У меня также была проблема с CommentedMapневозможностью изменения, но я не могу воспроизвести ее сейчас ... Я открою новый вопрос, если снова столкнусь с этой проблемой.
Холт
Вы должны использовать yaml = YAML(), вы получаете парсер / дампер туда и обратно, и это является производным от безопасного парсера / дампера, который знает о CommentedMap / Seq и т. Д.
Энтон,
14

Примечание : существует библиотека, основанная на следующем ответе, которая также реализует CLoader и CDumpers: 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
Эрик Нэсет
источник
Если вы хотите включить key_node.start_markатрибут в свое сообщение об ошибке, я не вижу очевидного способа упростить ваш центральный цикл построения. Если вы попытаетесь использовать тот факт, что OrderedDictконструктор будет принимать итерацию пар ключ-значение, вы потеряете доступ к этой детали при создании сообщения об ошибке.
ncoghlan
Кто-нибудь правильно тестировал этот код? Не могу заставить его работать в моем приложении!
theAlse 04
Пример использования: orders_dict = yaml.load ('' 'b: 1 a: 2' '', Loader = OrderedDictYAMLLoader) # orders_dict = OrderedDict ([('b', 1), ('a', 2)]) К сожалению мое редактирование сообщения было отклонено, поэтому, пожалуйста, извините за отсутствие форматирования.
Полковник Паник
Эта реализация нарушает загрузку упорядоченных типов сопоставления . Чтобы исправить это, вы можете просто удалить второй вызов add_constructorв своем __init__методе.
Райан
10

Обновление : библиотека устарела в пользу 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)
Алексей Чекунков
источник
Не знаю, тот же автор или нет, но посмотрите yodlна github.
Мистер Б.
3

В моей установке 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)
EricGreg
источник
Фактически это должно быть добавлено в апстрим.
Майкл
1
Юстед отправил запрос на перенос с вашими изменениями. github.com/yaml/pyyaml/pull/12 Будем надеяться на слияние.
Майкл
Очень хотелось бы, чтобы автор был более активным, последний коммит был 4 года назад. Это изменение было бы для меня находкой.
Марк Лемойн,
-1

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

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])
Адам Мерфи
источник