W Pythonie, jak można załadować mapowania YAML jako OrderedDicts?

132

Chciałbym, aby program ładujący PyYAML załadował mapowania (i uporządkowane mapowania) do typu Python 2.7+ OrderedDict , zamiast wanilii dicti listy par, których obecnie używa.

Jak najlepiej to zrobić?

Eric Naeseth
źródło

Odpowiedzi:

153

Aktualizacja: W Pythonie 3.6+ prawdopodobnie nie potrzebujesz OrderedDictw ogóle ze względu na nową implementację dykta , która jest używana w pypy od jakiegoś czasu (chociaż na razie uważana jest za szczegół implementacji CPythona).

Aktualizacja: W Pythonie 3.7+, natura zachowania kolejności wstawiania obiektów dict została zadeklarowana jako oficjalna część specyfikacji języka Python , zobacz Co nowego w Pythonie 3.7 .

Rozwiązanie @James podoba mi się za jego prostotę. Jednak zmienia domyślną yaml.Loaderklasę globalną , co może prowadzić do kłopotliwych skutków ubocznych. Szczególnie podczas pisania kodu biblioteki jest to zły pomysł. Ponadto nie działa bezpośrednio z yaml.safe_load().

Na szczęście rozwiązanie można poprawić bez większego wysiłku:

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)

W przypadku serializacji nie znam oczywistego uogólnienia, ale przynajmniej nie powinno to mieć żadnych skutków ubocznych:

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
źródło
3
+1 - bardzo za to dziękuję, zaoszczędziło mi to wielu kłopotów.
Nobilis,
2
Ta implementacja łamie tagi scalające YAML, BTW
Randy
1
@Randy Thanks. Wcześniej nie działałem w tym scenariuszu, ale teraz dodałem poprawkę, aby sobie z tym poradzić (mam nadzieję).
coldfix
9
@ArneBabenhauserheide Nie jestem pewien, czy PyPI jest wystarczająco upstream, ale spójrz na ruamel.yaml (jestem autorem tego), jeśli myślisz, że tak.
Anthon
1
@Anthon Twoja biblioteka ruamel.yaml działa bardzo dobrze. Dziękuję za to.
Jan Vlcinsky
57

Moduł yaml umożliwia określenie niestandardowych „reprezentantów” do konwersji obiektów Pythona na tekst i „konstruktorów” w celu odwrócenia tego procesu.

_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)
Brice M. Dempsey
źródło
6
jakieś wyjaśnienia dla tej odpowiedzi?
Shuman
1
Lub jeszcze lepiej, from six import iteritemsa następnie zmień to na iteritems(data)tak, aby działało równie dobrze w Pythonie 2 i 3.
Midnighter
5
Wygląda na to, że używa nieudokumentowanych funkcji PyYAML ( represent_dicti DEFAULT_MAPPING_TAG). Czy to dlatego, że dokumentacja jest niekompletna, czy te funkcje nie są obsługiwane i mogą ulec zmianie bez powiadomienia?
aldel
3
Należy pamiętać, że za dict_constructorjakość trzeba zadzwonić loader.flatten_mapping(node)lub nie będzie w stanie załadować <<: *...(seryjnej składni)
Anthony sottile
@ brice-m-dempsey czy możesz dodać jakiś przykład, jak używać twojego kodu? W moim przypadku nie wydaje się działać (Python 3.7)
schaffe
55

Opcja 2018:

oyamljest bezpośrednim zamiennikiem PyYAML, który zachowuje porządek dyktowania. Obsługiwane są zarówno Python 2, jak i Python 3. Po prostu pip install oyamli importuj, jak pokazano poniżej:

import oyaml as yaml

Nie będziesz już irytować zepsutymi mapowaniami podczas zrzutu / ładowania.

Uwaga: jestem autorem oyaml.

wim
źródło
1
Dziękuję Ci za to! Z jakiegoś powodu nawet w Pythonie 3.8 kolejność nie była przestrzegana w PyYaml. oyaml natychmiast rozwiązał ten problem.
John Smith Opcjonalnie
26

Opcja 2015 (i nowsze):

ruamel.yaml to kropla zastępująca PyYAML (zastrzeżenie: jestem autorem tego pakietu). Zachowanie kolejności mapowań było jedną z rzeczy dodanych w pierwszej wersji (0.1) w 2015 roku. Nie tylko zachowuje kolejność Twoich słowników, ale również zachowuje komentarze, nazwy kotwic, tagi i obsługuje YAML 1.2 specyfikacja (wydana 2009)

Specyfikacja mówi, że kolejność nie jest gwarantowana, ale oczywiście istnieje porządek w pliku YAML i odpowiedni parser może to po prostu zatrzymać i przejrzyście wygenerować obiekt, który zachowuje kolejność. Wystarczy wybrać odpowiedni parser, moduł ładujący i wywrotkę¹:

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)

da tobie:

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

datajest typu, CommentedMapktóry działa jak dykt, ale zawiera dodatkowe informacje, które są przechowywane do momentu zrzucenia (w tym zachowany komentarz!)

Anthon
źródło
To całkiem fajne, jeśli masz już plik YAML, ale jak to zrobić, używając struktury Pythona? Próbowałem użyć CommentedMapbezpośrednio, ale to nie działa i OrderedDictumieszcza !!omapwszędzie, co nie jest zbyt przyjazne dla użytkownika.
Holt
Nie jestem pewien, dlaczego CommentedMap nie działa dla Ciebie. Czy możesz zadać pytanie ze swoim (zminimalizowanym) kodem i otagować je ruamel.yaml? W ten sposób otrzymam powiadomienie i odpowiem.
Anthon
Przepraszam, myślę, że to dlatego, że próbowałem zapisać plik CommentedMapz safe=Truein YAML, który nie działał (używając safe=Falsedziała). Miałem również problem z CommentedMapbrakiem możliwości modyfikacji, ale nie mogę tego teraz odtworzyć ... Otworzę nowe pytanie, jeśli ponownie napotkam ten problem.
Holt
Powinieneś używać yaml = YAML(), otrzymujesz parser / zrzut w obie strony, który jest pochodną bezpiecznego parsera / zrzutu, który wie o CommentedMap / Seq itp.
Anthon
14

Uwaga : istnieje biblioteka oparta na następującej odpowiedzi, która implementuje również CLoader i CDumpers: Phynix / yamlloader

Bardzo wątpię, że to najlepszy sposób, aby to zrobić, ale to jest sposób, w jaki wymyśliłem i działa. Dostępne również w skrócie .

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
Eric Naeseth
źródło
Jeśli chcesz uwzględnić key_node.start_markatrybut w komunikacie o błędzie, nie widzę żadnego oczywistego sposobu na uproszczenie centralnej pętli konstrukcyjnej. Jeśli spróbujesz wykorzystać fakt, że OrderedDictkonstruktor zaakceptuje iterowalne pary klucz, wartość, utracisz dostęp do tego szczegółu podczas generowania komunikatu o błędzie.
ncoghlan
czy ktoś poprawnie przetestował ten kod? Nie mogę zmusić go do działania w mojej aplikacji!
theAlse
Przykładowe użycie: order_dict = yaml.load ('' 'b: 1 a: 2' '', Loader = OrderedDictYAMLLoader) # managed_dict = OrderedDict ([('b', 1), ('a', 2)]) Niestety moja edycja posta została odrzucona, więc przepraszam za brak formatowania.
Colonel Panic
Ta implementacja przerywa ładowanie uporządkowanych typów mapowania . Aby to naprawić, możesz po prostu usunąć drugie wywołanie add_constructorw swojej __init__metodzie.
Ryan
10

Aktualizacja : biblioteka została wycofana na korzyść yamlloadera (opartego na yamlordereddictloader)

Właśnie znalazłem bibliotekę Pythona ( https://pypi.python.org/pypi/yamlordereddictloader/0.1.1 ), która została utworzona na podstawie odpowiedzi na to pytanie i jest dość prosta w użyciu:

import yaml
import yamlordereddictloader

datas = yaml.load(open('myfile.yml'), Loader=yamlordereddictloader.Loader)
Alex Chekunkov
źródło
Nie wiem, czy to ten sam autor, czy nie, ale sprawdź yodlna github.
Mr. B
3

Podczas mojej instalacji For PyYaml dla Pythona 2.7 zaktualizowałem __init__.py, constructor.py i loader.py. Teraz obsługuje opcję object_pairs_hook dla poleceń ładowania. Różnica zmian, które wprowadziłem poniżej.

__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
źródło
Faktycznie, powinno to zostać dodane wcześniej.
Michael
1
Właśnie złożył żądanie ściągnięcia ze zmianami. github.com/yaml/pyyaml/pull/12 Miejmy nadzieję na połączenie.
Michael
Naprawdę chciałbym, żeby autor był bardziej aktywny, ostatnie zatwierdzenie miało miejsce 4 lata temu. Ta zmiana byłaby dla mnie darem niebios.
Mark LeMoine
-1

oto proste rozwiązanie, które sprawdza również, czy na mapie nie ma zduplikowanych kluczy najwyższego poziomu.

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])
Adam Murphy
źródło