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

Me gustaría hacer que el cargador de PyYAML cargue las asignaciones (y las asignaciones ordenadas) en el tipo de Pyreded 2.7+ OrderedDict , en lugar del dictado de vainilla y la lista de pares que utiliza actualmente.

¿Cuál es la mejor manera de hacer eso?

Actualización: en Python 3.6+ probablemente no necesite OrderedDict en absoluto debido a la nueva implementación de dict que ha estado en uso en pypy por algún tiempo (aunque se considera un detalle de implementación de CPython por ahora).

Actualización: en Python 3.7+, se ha declarado que la naturaleza de conservación de orden de inserción de los objetos dict es una parte oficial de la especificación de idioma de Python ; consulte Novedades en Python 3.7 .

Me gusta la solución de @James por su simplicidad. Sin embargo, cambia la clase global predeterminada de yaml.Loader , que puede llevar a efectos secundarios molestos. Especialmente, cuando se escribe el código de la biblioteca 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) 

El módulo yaml le permite especificar ‘representantes’ personalizados para convertir objetos de Python a 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) 

Opción 2018:

oyaml es un reemplazo directo para PyYAML que preserva el orden de los dictados . Python 2 y Python 3 son compatibles. Simplemente pip install oyaml e importe como se muestra a continuación:

 import oyaml as yaml 

Ya no estará molesto por las asignaciones arruinadas al descargar / cargar.

Nota: soy el autor de oyaml.

Opción 2015 (y posterior):

ruamel.yaml es un reemplazo para 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 sus diccionarios, sino que también conservará los comentarios, los nombres de los anclajes, las tags y es compatible con el YAML 1.2. especificación (lanzado 2009)

La especificación dice que el pedido no está garantizado, pero, por supuesto, hay un pedido en el archivo YAML y el analizador adecuado puede aferrarse a eso y generar de forma transparente un objeto que mantiene el pedido. Solo tiene que elegir el analizador, el cargador y el dumper adecuados:

 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 

data son del tipo CommentedMap que funciona como un dict, pero tiene información adicional que se conserva hasta que se vuelca (¡incluido el comentario conservado!)

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

Dudo mucho que esta sea la mejor manera de hacerlo, pero esta es la forma en que 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 

Actualización : la biblioteca ha quedado en desuso en 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) 

En mi instalación en PyYaml para Python 2.7 actualicé __init__.py, constructor.py y loader.py. Ahora soporta la opción object_pairs_hook para cargar comandos. La diferencia de cambios que he hecho está abajo.

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

Hay un boleto de PyYAML sobre el tema abierto hace 5 años. Contiene algunos enlaces relevantes, incluido el enlace a esta pregunta 🙂 Personalmente tomé el gist 317164 y lo modifiqué un poco para usar OrderedDict de Python 2.7, no la implementación incluida (simplemente reemplazé la clase con las from collections import OrderedDict ).

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