En Python, comment pouvez-vous charger des mappages YAML en tant que OrderedDicts?

128

J'aimerais que le chargeur de PyYAML charge les mappages (et les mappages ordonnés) dans le type OrderedDict Python 2.7+ , au lieu de la vanille dictet de la liste des paires qu'il utilise actuellement.

Quelle est la meilleure façon de faire cela?

Eric Naeseth
la source

Réponses:

147

Mise à jour: Dans python 3.6+, vous n'avez probablement pas besoin OrderedDictdu tout en raison de la nouvelle implémentation de dict qui est utilisée dans pypy depuis un certain temps (bien que considérée comme un détail de l'implémentation CPython pour l'instant).

Mise à jour: dans python 3.7+, la nature de préservation de l'ordre d'insertion des objets dict a été déclarée comme faisant partie officielle de la spécification du langage Python , voir Nouveautés de Python 3.7 .

J'aime la solution de @James pour sa simplicité. Cependant, cela change la yaml.Loaderclasse globale par défaut , ce qui peut entraîner des effets secondaires gênants. Surtout, lors de l'écriture de code de bibliothèque, c'est une mauvaise idée. De plus, cela ne fonctionne pas directement avec yaml.safe_load().

Heureusement, la solution peut être améliorée sans trop d'efforts:

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)

Pour la sérialisation, je ne connais pas de généralisation évidente, mais au moins cela ne devrait pas avoir d'effets secondaires:

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
la source
3
+1 - merci beaucoup pour cela, cela m'a évité tellement de problèmes.
Nobilis
2
Cette implémentation rompt les balises de fusion YAML, BTW
Randy
1
@Randy Merci. Je n'ai pas couru dans ce scénario auparavant, mais maintenant j'ai ajouté un correctif pour gérer cela également (j'espère).
coldfix
9
@ArneBabenhauserheide Je ne sais pas si PyPI est suffisamment en amont, mais jetez un œil à ruamel.yaml (j'en suis l'auteur) si vous pensez que c'est le cas.
Anthon
1
@Anthon Votre bibliothèque ruamel.yaml fonctionne très bien. Merci pour ça.
Jan Vlcinsky
56

Le module yaml vous permet de spécifier des «représentants» personnalisés pour convertir des objets Python en texte et des «constructeurs» pour inverser le processus.

_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
la source
5
des explications pour cette réponse?
Shuman
1
Ou encore mieux from six import iteritems, puis changez-le pour iteritems(data)qu'il fonctionne aussi bien en Python 2 et 3.
Midnighter
5
Cela semble utiliser des fonctionnalités non documentées de PyYAML ( represent_dictet DEFAULT_MAPPING_TAG). Est-ce parce que la documentation est incomplète ou est-ce que ces fonctionnalités ne sont pas prises en charge et sont susceptibles d'être modifiées sans préavis?
aldel
3
Notez que pour dict_constructorvous devrez appeler loader.flatten_mapping(node)ou vous ne pourrez pas charger <<: *...(syntaxe de fusion)
Anthony Sottile
@ brice-m-dempsey pouvez-vous ajouter un exemple comment utiliser votre code? Cela ne semble pas fonctionner dans mon cas (Python 3.7)
schaffe
53

Option 2018:

oyamlest un remplacement de PyYAML qui préserve l'ordre des dict. Python 2 et Python 3 sont pris en charge. Juste pip install oyamlet importez comme indiqué ci-dessous:

import oyaml as yaml

Vous ne serez plus ennuyé par les mappages foirés lors du déchargement / chargement.

Remarque: je suis l'auteur d'oyaml.

wim
la source
1
Merci pour ça! Pour une raison quelconque, même avec Python 3.8, l'ordre n'était pas respecté avec PyYaml. oyaml a résolu cela pour moi immédiatement.
John Smith Facultatif
26

Option 2015 (et ultérieure):

ruamel.yaml remplace PyYAML (avertissement: je suis l'auteur de ce package). Préserver l'ordre des mappages était l'une des choses ajoutées dans la première version (0.1) en 2015. Non seulement cela préserve l'ordre de vos dictionnaires, mais il conservera également les commentaires, les noms d'ancrage, les balises et prend en charge le YAML 1.2 spécification (publiée en 2009)

La spécification dit que l'ordre n'est pas garanti, mais bien sûr, il y a un ordre dans le fichier YAML et l'analyseur approprié peut simplement s'en tenir à cela et générer de manière transparente un objet qui conserve l'ordre. Il vous suffit de choisir le bon analyseur, chargeur et dumper¹:

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)

te donnera:

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

dataest du type CommentedMapqui fonctionne comme un dict, mais a des informations supplémentaires qui sont conservées jusqu'à ce qu'elles soient vidées (y compris le commentaire conservé!)

Anthon
la source
C'est plutôt bien si vous avez déjà un fichier YAML, mais comment faire cela en utilisant une structure Python? J'ai essayé de l'utiliser CommentedMapdirectement mais ça ne marche pas, et OrderedDictmet !!omappartout ce qui n'est pas très convivial.
Holt le
Je ne sais pas pourquoi CommentedMap n'a pas fonctionné pour vous. Pouvez-vous poster une question avec votre code (minimisé) et le taguer ruamel.yaml? De cette façon, je serai informé et je répondrai.
Anthon
Désolé, je pense que c'est parce que j'ai essayé d'enregistrer le CommentedMapavec safe=Truein YAML, qui n'a pas fonctionné (en utilisant des safe=Falseœuvres). J'ai également eu un problème de CommentedMapne pas être modifiable, mais je ne peux pas le reproduire maintenant ... J'ouvrirai une nouvelle question si je rencontre à nouveau ce problème.
Holt le
Vous devriez utiliser yaml = YAML(), vous obtenez l'analyseur / dumper aller-retour et qui est dérivé de l'analyseur / dumper sûr qui connaît CommentedMap / Seq etc.
Anthon
14

Remarque : il existe une bibliothèque, basée sur la réponse suivante, qui implémente également le CLoader et les CDumpers: Phynix / yamlloader

Je doute fort que ce soit la meilleure façon de le faire, mais c'est ainsi que j'ai proposé et cela fonctionne. Également disponible sous forme de résumé .

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
la source
Si vous souhaitez inclure l' key_node.start_markattribut dans votre message d'erreur, je ne vois aucun moyen évident de simplifier votre boucle de construction centrale. Si vous essayez d'utiliser le fait que le OrderedDictconstructeur acceptera un itérable de paires clé / valeur, vous perdez l'accès à ce détail lors de la génération du message d'erreur.
ncoghlan le
Quelqu'un a-t-il testé ce code correctement? Je n'arrive pas à le faire fonctionner dans mon application!
theAlse
Exemple d'utilisation: order_dict = yaml.load ('' 'b: 1 a: 2' '', Loader = OrderedDictYAMLLoader) # orders_dict = OrderedDict ([('b', 1), ('a', 2)]) Malheureusement ma modification du message a été rejetée, veuillez donc excuser le manque de formatage.
Colonel Panic
Cette implémentation interrompt le chargement des types de mappage ordonnés . Pour résoudre ce problème, vous pouvez simplement supprimer le deuxième appel à add_constructordans votre __init__méthode.
Ryan
10

Mise à jour : la bibliothèque est obsolète au profit du yamlloader (qui est basé sur le yamlordereddictloader)

Je viens de trouver une bibliothèque Python ( https://pypi.python.org/pypi/yamlordereddictloader/0.1.1 ) qui a été créée à partir des réponses à cette question et est assez simple à utiliser:

import yaml
import yamlordereddictloader

datas = yaml.load(open('myfile.yml'), Loader=yamlordereddictloader.Loader)
Alex Chekunkov
la source
Je ne sais pas si c'est le même auteur ou non, mais vérifiez yodlsur github.
M. B
3

Sur mon installation For PyYaml pour Python 2.7, j'ai mis à jour __init__.py, constructor.py et loader.py. Prend désormais en charge l'option object_pairs_hook pour les commandes de chargement. La différence des modifications que j'ai apportées est ci-dessous.

__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
la source
Cela devrait être ajouté en amont en fait.
Michael
1
Justed a déposé une demande d'extraction avec vos modifications. github.com/yaml/pyyaml/pull/12 Espérons une fusion.
Michael
J'aimerais vraiment que l'auteur soit plus actif, le dernier commit remonte à 4 ans. Ce changement serait une aubaine pour moi.
Mark LeMoine
-1

voici une solution simple qui vérifie également les clés de niveau supérieur dupliquées dans votre carte.

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
la source