No Python, como você pode carregar os mapeamentos YAML como OrderedDicts?

Respostas:

147

Atualização: No python 3.6+, você provavelmente não precisa de OrderedDicttodo devido à nova implementação do dict que está em uso no pypy há algum tempo (embora considerado o detalhe da implementação do CPython por enquanto).

Atualização: No python 3.7+, a natureza de preservação da ordem de inserção dos objetos dict foi declarada parte integrante da especificação da linguagem Python , consulte O que há de novo no Python 3.7 .

Eu como @ James solução para a sua simplicidade. No entanto, ele altera a yaml.Loaderclasse global padrão , o que pode levar a efeitos colaterais problemáticos. Especialmente, ao escrever o código da biblioteca, é uma má idéia. Além disso, ele não funciona diretamente yaml.safe_load().

Felizmente, a solução pode ser melhorada sem muito esforço:

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 serialização, não conheço uma generalização óbvia, mas pelo menos isso não deve ter efeitos colaterais:

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
fonte
3
+1 - muito obrigado por isso, me salvou muitos problemas.
Nobilis
2
Esta implementação quebra as tags de mesclagem YAML, BTW
Randy
1
@ Randy Obrigado. Eu não corri nesse cenário antes, mas agora adicionei uma correção para lidar com isso também (espero).
coldfix
9
@ArneBabenhauserheide Não tenho certeza se o PyPI está upstream o suficiente, mas dê uma olhada no ruamel.yaml (eu sou o autor disso) se você acha que sim.
Anthon
1
@Anthon Sua biblioteca ruamel.yaml funciona muito bem. Obrigado por isso.
Jan Vlcinsky
56

O módulo yaml permite especificar 'representantes' personalizados para converter objetos Python em texto e 'construtores' para reverter o 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)
Brice M. Dempsey
fonte
5
alguma explicação para esta resposta?
Shuman
1
Ou ainda melhor from six import iteritemse, em seguida, alterá-lo para iteritems(data)assim que ele funciona igualmente bem em Python 2 e 3.
Midnighter
5
Parece estar usando recursos não documentados do PyYAML ( represent_dicte DEFAULT_MAPPING_TAG). Isso ocorre porque a documentação está incompleta ou esses recursos não são suportados e estão sujeitos a alterações sem aviso prévio?
Aldel # 13/17
3
Note-se que para dict_constructorvocê vai precisar ligar loader.flatten_mapping(node)ou você não será capaz de carregar <<: *...(sintaxe merge)
Anthony Sottile
@ brice-m-dempsey você pode adicionar algum exemplo de como usar seu código? Não parece ao trabalho no meu caso (Python 3.7)
schaffe
53

2018 opção:

oyamlé um substituto para o PyYAML, que preserva a ordem dos ditados . O Python 2 e o Python 3 são suportados. Just pip install oyamle importe conforme mostrado abaixo:

import oyaml as yaml

Você não será mais incomodado por mapeamentos errados ao descarregar / carregar.

Nota: Eu sou o autor de oyaml.

wim
fonte
1
Obrigado por isso! Por alguma razão, mesmo no Python 3.8, o pedido não era respeitado no PyYaml. oyaml resolveu isso para mim imediatamente.
John Smith Opcional
26

Opção 2015 (e posterior):

ruamel.yaml é uma queda no substituto do PyYAML (exoneração de responsabilidade: eu sou o autor desse pacote). Preservar a ordem dos mapeamentos foi uma das coisas adicionadas na primeira versão (0.1) em 2015. Não apenas preserva a ordem dos seus dicionários, mas também preserva comentários, nomes de âncoras, tags e suporta o YAML 1.2 especificação (lançado em 2009)

A especificação diz que a ordem não é garantida, mas é claro que há pedidos no arquivo YAML e o analisador apropriado pode apenas se apegar a isso e gerar transparentemente um objeto que os mantenha. Você só precisa escolher o analisador, carregador e dumper certos¹:

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)

Darei à você:

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

dataé do tipo CommentedMapque funciona como um ditado, mas tem informações extras que são mantidas por aí até serem descartadas (incluindo o comentário preservado!)

Anthon
fonte
Isso é muito bom se você já possui um arquivo YAML, mas como você faz isso usando uma estrutura Python? Eu tentei usar CommentedMapdiretamente, mas ele não funciona, e OrderedDictcoloca em !!omaptodos os lugares que não é muito fácil de usar.
Holt
Não sei por que o CommentedMap não funcionou para você. Você pode postar uma pergunta com seu código (minimizado) e marcá-lo como ruamel.yaml? Dessa forma, serei notificado e responderei.
Anthon
Desculpe, acho que é porque tentei salvar o CommentedMapwith safe=Truein YAML, que não funcionou (usando o safe=Falseworks). Também tive problemas em CommentedMapnão ser modificável, mas não posso reproduzi-lo agora ... Vou abrir uma nova pergunta se encontrar esse problema novamente.
Holt
Você deve estar usando yaml = YAML(), você obtém o analisador / descarregador de ida e volta e isso é derivado do analisador / descarregador seguro que sabe sobre o CommentedMap / Seq etc.
Anthon
14

Nota : existe uma biblioteca, com base na seguinte resposta, que implementa também o CLoader e CDumpers: Phynix / yamlloader

Eu duvido muito que essa seja a melhor maneira de fazê-lo, mas essa é a maneira que eu criei e funciona. Também disponível como uma essência .

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
fonte
Se você deseja incluir o key_node.start_markatributo na sua mensagem de erro, não vejo nenhuma maneira óbvia de simplificar seu loop de construção central. Se você tentar fazer uso do fato de que o OrderedDictconstrutor aceitará uma iterável de pares de chave e valor, você perderá o acesso a esses detalhes ao gerar a mensagem de erro.
Ncoghlan
alguém testou esse código corretamente? Não consigo fazê-lo funcionar no meu aplicativo!
theAlse 4/13/13
Exemplo de uso: orders_dict = yaml.load ('' 'b: 1 a: 2' '', Loader = OrderedDictYAMLLoader) # orders_dict = OrderedDict ([('b', 1), ('a', 2)]) Infelizmente minha edição da postagem foi rejeitada. Desculpe a falta de formatação.
Coronel Panic
Essa implementação interrompe o carregamento dos tipos de mapeamento ordenados . Para corrigir isso, você pode simplesmente remover a segunda chamada add_constructorno seu __init__método.
21919 Ryan Ryan
10

Atualização : a biblioteca foi preterida em favor do yamlloader (que é baseado no yamlordereddictloader)

Acabei de encontrar uma biblioteca Python ( https://pypi.python.org/pypi/yamlordereddictloader/0.1.1 ) que foi criada com base nas respostas a esta pergunta e é bastante simples de usar:

import yaml
import yamlordereddictloader

datas = yaml.load(open('myfile.yml'), Loader=yamlordereddictloader.Loader)
Alex Chekunkov
fonte
Não sei se é o mesmo autor ou não, mas confira yodlno github.
Mr. B
3

Na instalação do For PyYaml para Python 2.7, atualizei __init__.py, constructor.py e loader.py. Agora suporta a opção object_pairs_hook para comandos de carregamento. A diferença das alterações que fiz está abaixo.

__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
fonte
Na verdade, isso deve ser adicionado a montante.
Michael
1
Justed apresentou uma solicitação de recebimento com suas alterações. github.com/yaml/pyyaml/pull/12 Vamos esperar uma fusão.
Michael
Realmente gostaria que o autor fosse mais ativo, o último commit foi há 4 anos. Essa mudança seria uma dádiva de Deus para mim.
precisa saber é o seguinte
-1

aqui está uma solução simples que também verifica chaves duplicadas de nível superior em seu 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
fonte