En Python, ¿cómo puede cargar asignaciones YAML como OrderedDicts?

Respuestas:

147

Actualización: en python 3.6+ probablemente no necesite OrderedDictnada debido a la nueva implementación de dict que ha estado en uso en pypy durante algún tiempo (aunque se considera detalles de implementación de CPython por ahora).

Actualización: en python 3.7+, la naturaleza de preservación del orden de inserción de los objetos dict se ha declarado como una parte oficial de la especificación del lenguaje Python , consulte Novedades de Python 3.7 .

Me gusta la solución de @James por su simplicidad. Sin embargo, cambia la yaml.Loaderclase global predeterminada , lo que puede provocar efectos secundarios problemáticos. Especialmente, al escribir código de biblioteca, esta es una mala idea. Además, no funciona directamente con yaml.safe_load().

Afortunadamente, la solución se puede mejorar sin mucho esfuerzo:

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)

Para la serialización, no conozco una generalización obvia, pero al menos esto no debería tener ningún efecto secundario:

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
fuente
3
+1: muchas gracias por esto, me ha ahorrado muchos problemas.
Nobilis
2
Esta implementación rompe las etiquetas de fusión YAML, por cierto
Randy
1
@Randy Gracias. No corrí en ese escenario antes, pero ahora agregué una solución para manejar esto también (espero).
coldfix
9
@ArneBabenhauserheide No estoy seguro de si PyPI está lo suficientemente arriba, pero eche un vistazo a ruamel.yaml (soy el autor de eso) si cree que lo hace.
Anthon
1
@Anthon Su biblioteca ruamel.yaml funciona muy bien. Gracias por eso.
Jan Vlcinsky
56

El módulo yaml le permite especificar 'representantes' personalizados para convertir objetos de Python en texto y 'constructores' para revertir el proceso.

_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
fuente
55
alguna explicación para esta respuesta?
Shuman
1
O incluso mejor from six import iteritemsy luego cambiarlo para iteritems(data)que funcione igual de bien en Python 2 y 3.
Midnighter
55
Esto parece estar usando características no documentadas de PyYAML ( represent_dicty DEFAULT_MAPPING_TAG). ¿Esto se debe a que la documentación está incompleta o estas funciones no son compatibles y están sujetas a cambios sin previo aviso?
aldel
3
Tenga en cuenta que dict_constructortendrá que llamar loader.flatten_mapping(node)o no podrá cargar <<: *...(combinar la sintaxis)
Anthony Sottile
@ brice-m-dempsey ¿puedes agregar algún ejemplo de cómo usar tu código? No parece funcionar en mi caso (Python 3.7)
schaffe
53

Opción 2018:

oyamles un reemplazo directo para PyYAML que conserva el pedido de dict. Python 2 y Python 3 son compatibles. Solo pip install oyaml, e importe como se muestra a continuación:

import oyaml as yaml

Ya no te molestarán las asignaciones arruinadas al descargar / cargar.

Nota: soy el autor de oyaml.

wim
fuente
1
¡Gracias por esto! Por alguna razón, incluso con Python 3.8, el orden no se respetó con PyYaml. oyaml me resolvió esto de inmediato.
John Smith Opcional
26

Opción 2015 (y posterior):

ruamel.yaml es una caída en el reemplazo de PyYAML (descargo de responsabilidad: soy el autor de ese paquete). Preservar el orden de las asignaciones fue una de las cosas que se agregaron en la primera versión (0.1) en 2015. No solo conserva el orden de los diccionarios, sino que también conserva los comentarios, los nombres de anclaje, las etiquetas y es compatible con YAML 1.2 especificación (lanzado en 2009)

La especificación dice que el orden no está garantizado, pero, por supuesto, hay un orden en el archivo YAML y el analizador apropiado puede retenerlo y generar de forma transparente un objeto que mantenga el orden. Solo tiene que elegir el analizador, el cargador y el volcador correctos¹:

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 regalaré:

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

dataes de tipo CommentedMapque funciona como un dict, pero tiene información adicional que se mantiene hasta que se descarta (¡incluido el comentario conservado!)

Anthon
fuente
Eso es bastante bueno si ya tienes un archivo YAML, pero ¿cómo lo haces usando una estructura Python? Intenté usarlo CommentedMapdirectamente pero no funciona, y lo OrderedDictcoloca en !!omaptodas partes, lo que no es muy fácil de usar.
Holt
No estoy seguro de por qué CommentedMap no funcionó para usted. ¿Puedes publicar una pregunta con tu código (minimalista) y etiquetarla como ruamel.yaml? De esa manera seré notificado y responderé.
Anthon
Lo siento, creo que es porque traté de guardar CommentedMapcon safe=Truein YAML, que no funcionó (usando safe=Falseworks). También tuve problemas para CommentedMapno ser modificable, pero no puedo reproducirlo ahora ... Abriré una nueva pregunta si encuentro este problema nuevamente.
Holt
Deberías estar usando yaml = YAML(), obtienes el analizador / volcador de ida y vuelta y eso es un derivado del analizador / volcador seguro que sabe sobre CommentedMap / Seq, etc.
Anthon
14

Nota : hay una biblioteca, basada en la siguiente respuesta, que implementa también CLoader y CDumpers: Phynix / yamlloader

Dudo mucho que esta sea la mejor manera de hacerlo, pero así es como se me ocurrió, y funciona. También disponible como una esencia .

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
fuente
Si desea incluir el key_node.start_markatributo en su mensaje de error, no veo ninguna forma obvia de simplificar su ciclo de construcción central. Si intenta utilizar el hecho de que el OrderedDictconstructor aceptará un iterable de pares clave, valor, perderá el acceso a ese detalle al generar el mensaje de error.
ncoghlan
¿Alguien ha probado este código correctamente? ¡No puedo hacer que funcione en mi aplicación!
theAlse
Ejemplo de uso: shown_dict = yaml.load ('' 'b: 1 a: 2' '', Loader = OrderedDictYAMLLoader) # shown_dict = OrderedDict ([('b', 1), ('a', 2)]) Desafortunadamente mi edición de la publicación fue rechazada, así que disculpe la falta de formato.
Coronel Panic el
Esta implementación interrumpe la carga de los tipos de mapeo ordenados . Para solucionar esto, puede eliminar la segunda llamada a add_constructorsu __init__método.
Ryan
10

Actualización : la biblioteca quedó en desuso a favor del yamlloader (que se basa en el yamlordereddictloader)

Acabo de encontrar una biblioteca de Python ( https://pypi.python.org/pypi/yamlordereddictloader/0.1.1 ) que se creó en base a las respuestas a esta pregunta y es bastante simple de usar:

import yaml
import yamlordereddictloader

datas = yaml.load(open('myfile.yml'), Loader=yamlordereddictloader.Loader)
Alex Chekunkov
fuente
No sé si es el mismo autor o no, pero echa un vistazo a yodlgithub.
Sr. B
3

En mi instalación de For PyYaml para Python 2.7, actualicé __init__.py, constructor.py y loader.py. Ahora admite la opción object_pairs_hook para los comandos de carga. La diferencia de los cambios que hice está a continuación.

__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
fuente
Esto debería agregarse aguas arriba en realidad.
Michael
1
Justed presentó una solicitud de extracción con sus cambios. github.com/yaml/pyyaml/pull/12 Esperemos una fusión.
Michael
Realmente desearía que el autor fuera más activo, el último compromiso fue hace 4 años. Este cambio sería un regalo del cielo para mí.
Mark LeMoine
-1

Aquí hay una solución simple que también busca claves duplicadas de nivel superior en su mapa.

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
fuente