Як у Python, як можна завантажувати YAML-відображення як OrdersDicts?


128

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

Який найкращий спосіб зробити це?

Відповіді:


147

Оновлення: у 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)

3
+1 - дуже дякую за це, це врятувало мені стільки неприємностей.
Nobilis

2
Ця реалізація розбиває теги злиття YAML, BTW
Randy

1
@Randy Дякую Я раніше не працював у такому сценарії, але тепер я додав виправлення, щоб вирішити це також (я сподіваюся).
холодний виправлення

9
@ArneBabenhauserheide Я не впевнений, чи PyPI достатньо за течією, але погляньте на ruamel.yaml (я автор цього), якщо ви думаєте, що це так.
Антон

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). Це тому, що документація неповна, або ці функції не підтримуються і можуть бути змінені без попереднього повідомлення?
альдель

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.


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який функціонує як дикт, але має додаткову інформацію, яка зберігається навколо, поки не буде скинуто (включаючи збережений коментар!)


Це дуже добре, якщо у вас вже є файл YAML, але як це зробити за допомогою структури Python? Я спробував використовувати CommentedMapбезпосередньо, але це не працює, і OrderedDictставить !!omapвсюди, що не дуже зручно для користувачів.
Холт

Я не впевнений, чому CommentedMap не працює для вас. Чи можете ви опублікувати питання зі своїм (мінімальним) кодом і позначити його ruamel.yaml? Таким чином я отримаю сповіщення та відповідь.
Антон

Вибачте, я думаю, що це тому, що я спробував зберегти CommentedMapс, safe=Trueу YAMLякому не вийшло (використовуючи safe=Falseроботи). У мене також виникло питання про CommentedMapте, що його не можна змінити, але я не можу його відтворити зараз ... Я відкрию нове запитання, якщо я знову зустрінусь із цією проблемою.
Холт

Ви повинні використовувати yaml = YAML(), ви отримуєте аналізатор / самоскид в зворотному напрямку, який є похідним від безпечного аналізатора / самоскиду, який знає про CommentedMap / Seq тощо.
Anthon

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

Приклад використання: order_dict = yaml.load ('' 'b: 1 a: 2' '', Loader = OrdersDictYAMLLoader) # order_dict = OrdersDict ([('b', 1), ('a', 2)]) На жаль мою редакцію допису було відхилено, тож пробачте про відсутність форматування.
Полковник Паніка

Ця реалізація порушує завантаження впорядкованих типів відображення . Щоб виправити це, ви можете просто видалити другий дзвінок add_constructorу своєму __init__методі.
Райан

10

Оновлення : бібліотека була застаріла на користь завантажувача yamlloader (що базується на yamlordeddictloader)

Щойно я знайшов бібліотеку 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

У моїй установці 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)

Це слід додати насправді.
Майкл

1
Justed подав запит на тягнення із змінами. 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])
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.