In Python, come puoi caricare i mapping YAML come OrderedDicts?


Risposte:


147

Aggiornamento: in Python 3.6+ probabilmente non è necessario OrderedDictaffatto a causa della nuova implementazione di dict che è stata in uso in pypy per un po 'di tempo (anche se per ora considerato i dettagli dell'implementazione di CPython).

Aggiornamento: in Python 3.7+, la natura di conservazione dell'ordine di inserimento degli oggetti dict è stata dichiarata parte ufficiale delle specifiche del linguaggio Python , vedere Novità di Python 3.7 .

Mi piace la soluzione di @James per la sua semplicità. Tuttavia, cambia la yaml.Loaderclasse globale predefinita , che può portare a effetti collaterali fastidiosi. Soprattutto, quando si scrive il codice della libreria questa è una cattiva idea. Inoltre, non funziona direttamente con yaml.safe_load().

Fortunatamente, la soluzione può essere migliorata senza troppi sforzi:

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)

Per la serializzazione, non conosco un'ovvia generalizzazione, ma almeno questo non dovrebbe avere effetti collaterali:

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 - grazie mille per questo, mi ha risparmiato così tanti problemi.
Nobilis,

2
Questa implementazione interrompe i tag di unione YAML, BTW
Randy,

1
@Randy Grazie. Non ero mai stato in quello scenario prima, ma ora ho aggiunto una correzione per gestire anche questo (spero).
coldfix,

9
@ArneBabenhauserheide Non sono sicuro che PyPI sia abbastanza a monte, ma dai un'occhiata a ruamel.yaml (ne sono l'autore) se pensi che lo faccia.
Anthon,

1
@Anthon La tua libreria ruamel.yaml funziona molto bene. Grazie per quello
Jan Vlcinsky,

56

Il modulo yaml consente di specificare "rappresentatori" personalizzati per convertire oggetti Python in testo e "costruttori" per invertire il processo.

_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
qualche spiegazione per questa risposta?
Shuman

1
O ancora meglio from six import iteritemse poi cambiarlo in iteritems(data)modo che funzioni ugualmente bene in Python 2 e 3.
Midnighter

5
Questo sembra utilizzare le funzionalità non documentate di PyYAML ( represent_dicte DEFAULT_MAPPING_TAG). Questo perché la documentazione è incompleta o queste funzionalità non sono supportate e sono soggette a modifiche senza preavviso?
aldel,

3
Tieni presente che dict_constructordovrai chiamare loader.flatten_mapping(node)o non sarai in grado di caricare <<: *...(unire la sintassi)
Anthony Sottile

@ brice-m-dempsey puoi aggiungere qualche esempio su come usare il tuo codice? Non sembra funzionare nel mio caso (Python 3.7)
schaffe

53

Opzione 2018:

oyamlè un sostituto drop-in per PyYAML che preserva l'ordinamento del dict. Sono supportati sia Python 2 che Python 3. Basta pip install oyamlimportare come mostrato di seguito:

import oyaml as yaml

Non sarai più infastidito dalle mappature sbagliate durante lo scaricamento / caricamento.

Nota: sono l'autore di Oyaml.


1
Grazie per questo! Per qualche motivo, anche con Python 3.8 l'ordine non è stato rispettato con PyYaml. oyaml mi ha risolto questo problema immediatamente.
John Smith Opzionale

26

Opzione 2015 (e successive):

ruamel.yaml sostituisce PyYAML (dichiarazione di non responsabilità: sono l'autore di quel pacchetto). Preservare l'ordine delle mappature è stata una delle cose aggiunte nella prima versione (0.1) nel 2015. Non solo preserva l'ordine dei dizionari, ma preserverà anche commenti, nomi di ancoraggio, tag e supporterà YAML 1.2 specifica (rilasciata nel 2009)

La specifica dice che l'ordinamento non è garantito, ma ovviamente c'è l'ordinamento nel file YAML e il parser appropriato può semplicemente trattenerlo e generare in modo trasparente un oggetto che mantiene l'ordinamento. Hai solo bisogno di scegliere il giusto parser, caricatore e 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)

ti darà:

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

dataè di tipo CommentedMapche funziona come un dict, ma ha informazioni extra che vengono conservate fino a quando non vengono scaricate (incluso il commento conservato!)


È carino se hai già un file YAML, ma come puoi farlo usando una struttura Python? Ho provato a usarlo CommentedMapdirettamente ma non funziona e OrderedDictmette !!omapovunque che non sia molto user-friendly.
Holt

Non sono sicuro del motivo per cui CommentedMap non ha funzionato per te. Puoi pubblicare una domanda con il tuo codice (minimizzato) e taggarlo ruamel.yaml? In questo modo riceverò una notifica e risponderò.
Anthon,

Ci dispiace, penso sia perché ho provato a salvare CommentedMapcon safe=Truein YAML, che non ha funzionato (usando safe=Falselavori). Ho anche avuto problemi a CommentedMapnon essere modificabile, ma non riesco a riprodurlo ora ... Aprirò una nuova domanda se incontro di nuovo questo problema.
Holt,

Dovresti usare yaml = YAML(), ottieni il parser / dumper di andata e ritorno e che è derivato dal parser / dumper sicuro che conosce CommentedMap / Seq ecc.
Anthon,

14

Nota : esiste una libreria, basata sulla seguente risposta, che implementa anche CLoader e CDumpers: Phynix / yamlloader

Dubito fortemente che questo sia il modo migliore per farlo, ma questo è il modo in cui mi sono inventato e funziona. Disponibile anche come sostanza .

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

Se vuoi includere l' key_node.start_markattributo nel tuo messaggio di errore, non vedo alcun modo ovvio per semplificare il tuo ciclo di costruzione centrale. Se si tenta di sfruttare il fatto che il OrderedDictcostruttore accetterà un iterabile di coppie chiave, valore, si perde l'accesso a quel dettaglio quando si genera il messaggio di errore.
ncoghlan,

qualcuno ha testato correttamente questo codice? Non riesco a farlo funzionare nella mia applicazione!
theAlse

Esempio di utilizzo: order_dict = yaml.load ('' 'b: 1 a: 2' '', Loader = OrderedDictYAMLLoader) # ordinato_dict = OrderedDict ([('b', 1), ('a', 2)]) Sfortunatamente la mia modifica al post è stata rifiutata, quindi scusate la mancanza di formattazione.
Colonnello Panic,

Questa implementazione interrompe il caricamento dei tipi di mapping ordinati . Per risolvere questo problema, puoi semplicemente rimuovere la seconda chiamata add_constructornel tuo __init__metodo.
Ryan,

10

Aggiornamento : la libreria è stata deprecata a favore di yamlloader (che si basa su yamlordereddictloader)

Ho appena trovato una libreria Python ( https://pypi.python.org/pypi/yamlordereddictloader/0.1.1 ) che è stata creata sulla base delle risposte a questa domanda ed è abbastanza semplice da usare:

import yaml
import yamlordereddictloader

datas = yaml.load(open('myfile.yml'), Loader=yamlordereddictloader.Loader)

Non so se è lo stesso autore o no, ma controlla yodlsu Github.
Mr. B,

3

Nella mia installazione di For PyYaml per Python 2.7 ho aggiornato __init__.py, constructor.py e loader.py. Ora supporta l'opzione object_pairs_hook per i comandi di caricamento. La differenza tra le modifiche apportate è di seguito.

__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)

Questo dovrebbe essere effettivamente aggiunto a monte.
Michael,

1
Justed ha presentato una richiesta pull con le tue modifiche. github.com/yaml/pyyaml/pull/12 Speriamo in una fusione.
Michael,

Vorrei davvero che l'autore fosse più attivo, l'ultimo commit è stato 4 anni fa. Questo cambiamento sarebbe una manna dal cielo per me.
Mark LeMoine,

-1

ecco una semplice soluzione che verifica anche la presenza di chiavi duplicate di livello superiore nella mappa.

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])
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.