Wie können Sie in Python YAML-Zuordnungen als OrderedDicts laden?

128

Ich möchte PyYAMLs Loader dazu bringen, Mappings (und geordnete Mappings) in den Python 2.7+ OrderedDict- Typ zu laden , anstelle der Vanille dictund der Liste der Paare, die derzeit verwendet werden.

Was ist der beste Weg das zu tun?

Eric Naeseth
quelle

Antworten:

147

Update: In Python 3.6+ benötigen Sie wahrscheinlich OrderedDictaufgrund der neuen Dikt-Implementierung , die seit einiger Zeit in Pypy verwendet wird, überhaupt nichts (obwohl dies vorerst als CPython-Implementierungsdetail betrachtet wird).

Update: In Python 3.7+ wurde die Beibehaltung der Einfügungsreihenfolge von diktierten Objekten als offizieller Bestandteil der Python-Sprachspezifikation deklariert (siehe Neue Funktionen in Python 3.7) .

Ich mag die Lösung von @James wegen ihrer Einfachheit. Es ändert jedoch die globale Standardklasse yaml.Loader, was zu störenden Nebenwirkungen führen kann. Insbesondere beim Schreiben von Bibliothekscode ist dies eine schlechte Idee. Es funktioniert auch nicht direkt mit yaml.safe_load().

Glücklicherweise kann die Lösung ohne großen Aufwand verbessert werden:

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)

Für die Serialisierung kenne ich keine offensichtliche Verallgemeinerung, aber zumindest sollte dies keine Nebenwirkungen haben:

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
quelle
3
+1 - vielen Dank dafür, es hat mir so viel Ärger erspart.
Nobilis
2
Diese Implementierung bricht YAML Merge Tags, BTW
Randy
1
@ Randy Danke. Ich bin in diesem Szenario vorher nicht gelaufen, aber jetzt habe ich einen Fix hinzugefügt, um dies auch zu handhaben (ich hoffe).
Coldfix
9
@ArneBabenhauserheide Ich bin mir nicht sicher, ob PyPI stromaufwärts genug ist, aber schauen Sie sich ruamel.yaml (ich bin der Autor davon) an, wenn Sie glauben, dass dies der Fall ist.
Anthon
1
@Anthon Ihre ruamel.yaml Bibliothek funktioniert sehr gut. Dank dafür.
Jan Vlcinsky
56

Mit dem yaml-Modul können Sie benutzerdefinierte 'Repräsentanten' angeben, um Python-Objekte in Text zu konvertieren, und 'Konstruktoren', um den Prozess umzukehren.

_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
quelle
5
irgendwelche Erklärungen für diese Antwort?
Shuman
1
Oder noch besser from six import iteritemsund ändern Sie es dann iteritems(data)so, dass es in Python 2 & 3 gleich gut funktioniert.
Midnighter
5
Dies scheint undokumentierte Funktionen von PyYAML ( represent_dictund DEFAULT_MAPPING_TAG) zu verwenden. Liegt dies daran, dass die Dokumentation unvollständig ist, oder werden diese Funktionen nicht unterstützt und können ohne vorherige Ankündigung geändert werden?
Aldel
3
Beachten Sie, dass dict_constructorSie anrufen müssen loader.flatten_mapping(node)oder nicht laden können <<: *...(Syntax zusammenführen)
Anthony Sottile
@ brice-m-dempsey Kannst du ein Beispiel hinzufügen, wie du deinen Code verwendest? Es scheint in meinem Fall nicht zu funktionieren (Python 3.7)
schaffe
53

Option 2018:

oyamlist ein Ersatz für PyYAML, der die Reihenfolge der Diktate beibehält . Sowohl Python 2 als auch Python 3 werden unterstützt. Einfach pip install oyamlund wie unten gezeigt importieren:

import oyaml as yaml

Sie werden sich beim Dumping / Laden nicht mehr über vermasselte Zuordnungen ärgern.

Hinweis: Ich bin der Autor von Oyaml.

wim
quelle
1
Danke dafür! Aus irgendeinem Grund wurde die Reihenfolge auch mit Python 3.8 bei PyYaml nicht eingehalten. oyaml hat das sofort für mich gelöst.
John Smith Optional
26

Option 2015 (und später):

ruamel.yaml ist ein Ersatz für PyYAML (Haftungsausschluss: Ich bin der Autor dieses Pakets). Das Beibehalten der Reihenfolge der Zuordnungen war eines der Dinge, die in der ersten Version (0.1) im Jahr 2015 hinzugefügt wurden. Es behält nicht nur die Reihenfolge Ihrer Wörterbücher bei, sondern auch Kommentare, Ankernamen, Tags und unterstützt YAML 1.2 Spezifikation (veröffentlicht 2009)

Die Spezifikation besagt, dass die Reihenfolge nicht garantiert ist, aber natürlich gibt es eine Reihenfolge in der YAML-Datei, und der entsprechende Parser kann dies einfach beibehalten und transparent ein Objekt generieren, das die Reihenfolge beibehält. Sie müssen nur den richtigen Parser, Lader und Dumper auswählen¹:

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)

werde dir geben:

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

dataist vom Typ, CommentedMapder wie ein Diktat funktioniert, aber zusätzliche Informationen enthält, die bis zum Ablegen aufbewahrt werden (einschließlich des erhaltenen Kommentars!)

Anthon
quelle
Das ist ziemlich schön, wenn Sie bereits eine YAML-Datei haben, aber wie machen Sie das mit einer Python-Struktur? Ich habe versucht, CommentedMapdirekt zu verwenden, aber es funktioniert nicht und OrderedDictsetzt !!omapüberall, was nicht sehr benutzerfreundlich ist.
Holt
Ich bin mir nicht sicher, warum CommentedMap bei Ihnen nicht funktioniert hat. Können Sie eine Frage mit Ihrem (minimalisierten) Code posten und sie mit ruamel.yaml markieren? Auf diese Weise werde ich benachrichtigt und antworte.
Anthon
Entschuldigung, ich denke, das liegt daran, dass ich versucht habe, das CommentedMapwith safe=Truein zu speichern YAML, was nicht funktioniert hat (mithilfe von safe=Falseworks). Ich hatte auch ein Problem damit, CommentedMapnicht modifizierbar zu sein, aber ich kann es jetzt nicht reproduzieren ... Ich werde eine neue Frage öffnen, wenn ich erneut auf dieses Problem stoße.
Holt
Sie sollten verwenden yaml = YAML(), Sie erhalten den Round-Trip-Parser / Dumper und das ist eine Ableitung des sicheren Parsers / Dumper, der über CommentedMap / Seq usw. Bescheid weiß
Anthon
14

Hinweis : Es gibt eine Bibliothek, die auf der folgenden Antwort basiert und auch CLoader und CDumpers implementiert: Phynix / yamlloader

Ich bezweifle sehr, dass dies der beste Weg ist, aber das ist der Weg, den ich mir ausgedacht habe, und es funktioniert. Auch als Kernstück erhältlich .

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
quelle
Wenn Sie das key_node.start_markAttribut in Ihre Fehlermeldung aufnehmen möchten , sehe ich keinen offensichtlichen Weg, um Ihre zentrale Konstruktionsschleife zu vereinfachen. Wenn Sie versuchen, die Tatsache zu nutzen, dass der OrderedDictKonstruktor eine Iteration von Schlüssel-Wert-Paaren akzeptiert, verlieren Sie beim Generieren der Fehlermeldung den Zugriff auf dieses Detail.
Ncoghlan
Hat jemand diesen Code richtig getestet? Ich kann es in meiner Anwendung nicht zum Laufen bringen!
theAlse
Beispielverwendung: ordered_dict = yaml.load ('' 'b: 1 a: 2' '', Loader = OrderedDictYAMLLoader) # ordered_dict = OrderedDict ([('b', 1), ('a', 2)]) Leider Meine Bearbeitung des Beitrags wurde abgelehnt. Bitte entschuldigen Sie die fehlende Formatierung.
Colonel Panic
Diese Implementierung unterbricht das Laden geordneter Zuordnungstypen . Um dies zu beheben, können Sie einfach den zweiten Aufruf von add_constructorin Ihrer __init__Methode entfernen .
Ryan
10

Update : Die Bibliothek wurde zugunsten des yamlloader (der auf dem yamlordereddictloader basiert) veraltet.

Ich habe gerade eine Python-Bibliothek gefunden ( https://pypi.python.org/pypi/yamlordereddictloader/0.1.1 ), die basierend auf Antworten auf diese Frage erstellt wurde und recht einfach zu verwenden ist:

import yaml
import yamlordereddictloader

datas = yaml.load(open('myfile.yml'), Loader=yamlordereddictloader.Loader)
Alex Chekunkov
quelle
Ich weiß nicht, ob es derselbe Autor ist oder nicht, aber yodlschauen Sie sich Github an.
Herr B
3

Bei meiner For PyYaml-Installation für Python 2.7 habe ich __init__.py, constructor.py und loader.py aktualisiert. Unterstützt jetzt die Option object_pairs_hook für Ladebefehle. Die verschiedenen Änderungen, die ich vorgenommen habe, sind unten aufgeführt.

__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
quelle
Dies sollte tatsächlich vorgelagert hinzugefügt werden.
Michael
1
Justed hat eine Pull-Anfrage mit Ihren Änderungen eingereicht. github.com/yaml/pyyaml/pull/12 Hoffen wir auf eine Fusion.
Michael
Ich wünschte wirklich, der Autor wäre aktiver, das letzte Commit war vor 4 Jahren. Diese Änderung wäre ein Glücksfall für mich.
Mark LeMoine
-1

Hier ist eine einfache Lösung, die auch nach doppelten Schlüsseln der obersten Ebene in Ihrer Karte sucht.

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
quelle