web-dev-qa-db-fra.com

Comment lire un python Tuple en utilisant PyYAML?

J'ai le fichier YAML suivant nommé input.yaml:

cities:
  1: [0,0]
  2: [4,0]
  3: [0,4]
  4: [4,4]
  5: [2,2]
  6: [6,2]
highways:
  - [1,2]
  - [1,3]
  - [1,5]
  - [2,4]
  - [3,4]
  - [5,4]
start: 1
end: 4

Je le charge en utilisant PyYAML et j'imprime le résultat comme suit:

import yaml

f = open("input.yaml", "r")
data = yaml.load(f)
f.close()

print(data)

Le résultat est la structure de données suivante:

{ 'cities': { 1: [0, 0]
            , 2: [4, 0]
            , 3: [0, 4]
            , 4: [4, 4]
            , 5: [2, 2]
            , 6: [6, 2]
            }
, 'highways': [ [1, 2]
              , [1, 3]
              , [1, 5]
              , [2, 4]
              , [3, 4]
              , [5, 4]
              ]
, 'start': 1
, 'end': 4
}

Comme vous pouvez le voir, chaque ville et autoroute est représentée sous forme de liste. Cependant, je veux qu'ils soient représentés comme un tuple. Par conséquent, je les convertis manuellement en tuples en utilisant des compréhensions:

import yaml

f = open("input.yaml", "r")
data = yaml.load(f)
f.close()

data["cities"] = {k: Tuple(v) for k, v in data["cities"].items()}
data["highways"] = [Tuple(v) for v in data["highways"]]

print(data)

Cependant, cela semble être un hack. Existe-t-il un moyen de demander à PyYAML de les lire directement sous forme de tuples au lieu de listes?

20
Aadit M Shah

Je ne qualifierais pas ce que vous avez fait de hacky pour ce que vous essayez de faire. D'après ma compréhension, votre approche alternative consiste à utiliser des balises spécifiques à python dans votre fichier YAML afin qu'il soit représenté de manière appropriée lors du chargement du fichier yaml. Cependant, cela vous oblige à modifier votre fichier yaml qui, s'il est énorme, va probablement être assez irritant et pas idéal.

Regardez le PyYaml doc qui illustre encore cela. En fin de compte, vous voulez placer un !!python/Tuple devant votre structure que vous souhaitez représenter comme telle. Pour prendre vos échantillons de données, il souhaite:

FICHIER YAML:

cities:
  1: !!python/Tuple [0,0]
  2: !!python/Tuple [4,0]
  3: !!python/Tuple [0,4]
  4: !!python/Tuple [4,4]
  5: !!python/Tuple [2,2]
  6: !!python/Tuple [6,2]
highways:
  - !!python/Tuple [1,2]
  - !!python/Tuple [1,3]
  - !!python/Tuple [1,5]
  - !!python/Tuple [2,4]
  - !!python/Tuple [3,4]
  - !!python/Tuple [5,4]
start: 1
end: 4

Exemple de code:

import yaml

with open('y.yaml') as f:
    d = yaml.load(f.read())

print(d)

Qui produira:

{'cities': {1: (0, 0), 2: (4, 0), 3: (0, 4), 4: (4, 4), 5: (2, 2), 6: (6, 2)}, 'start': 1, 'end': 4, 'highways': [(1, 2), (1, 3), (1, 5), (2, 4), (3, 4), (5, 4)]}
16
idjaw

Selon la provenance de votre entrée YAML, votre "hack" est une bonne solution, surtout si vous utilisez yaml.safe_load() au lieu de la fonction non sécurisée yaml.load(). Si seules les séquences "feuilles" de votre fichier YAML doivent être des tuples, vous pouvez le faire ¹:

import pprint
import ruamel.yaml
from ruamel.yaml.constructor import SafeConstructor


def construct_yaml_Tuple(self, node):
    seq = self.construct_sequence(node)
    # only make "leaf sequences" into tuples, you can add dict 
    # and other types as necessary
    if seq and isinstance(seq[0], (list, Tuple)):
        return seq
    return Tuple(seq)

SafeConstructor.add_constructor(
    u'tag:yaml.org,2002:seq',
    construct_yaml_Tuple)

with open('input.yaml') as fp:
    data = ruamel.yaml.safe_load(fp)
pprint.pprint(data, width=24)

qui imprime:

{'cities': {1: (0, 0),
            2: (4, 0),
            3: (0, 4),
            4: (4, 4),
            5: (2, 2),
            6: (6, 2)},
 'end': 4,
 'highways': [(1, 2),
              (1, 3),
              (1, 5),
              (2, 4),
              (3, 4),
              (5, 4)],
 'start': 1}

si vous devez ensuite traiter plus de matériel où la séquence doit être à nouveau des listes "normales", utilisez:

SafeConstructor.add_constructor(
    u'tag:yaml.org,2002:seq',
    SafeConstructor.construct_yaml_seq)

¹ Cela a été fait en utilisant ruamel.yaml un analyseur YAML 1.2, dont je suis l'auteur. Vous devriez pouvoir faire de même avec l'ancien PyYAML si vous n'avez besoin que de prendre en charge YAML 1.1 et/ou ne pouvez pas mettre à niveau pour une raison quelconque

3
Anthon

J'ai rencontré le même problème que la question et je n'ai pas été trop satisfait des deux réponses. En parcourant la documentation de pyyaml, j'ai trouvé vraiment deux méthodes intéressantes yaml.add_constructor Et yaml.add_implicit_resolver.

Le résolveur implicite résout le problème d'avoir à baliser toutes les entrées avec !!python/Tuple En faisant correspondre les chaînes avec une expression régulière. Je voulais également utiliser la syntaxe Tuple, donc écrire Tuple: (10,120) au lieu d'écrire une liste Tuple: [10,120] Qui est ensuite convertie en Tuple, j'ai personnellement trouvé cela très ennuyeux. Je ne voulais pas non plus installer de bibliothèque externe. Voici le code:

import yaml
import re

# this is to convert the string written as a Tuple into a python Tuple
def yml_Tuple_constructor(loader, node): 
    # this little parse is really just for what I needed, feel free to change it!                                                                                            
    def parse_tup_el(el):                                                                                                            
        # try to convert into int or float else keep the string                                                                      
        if el.isdigit():                                                                                                             
            return int(el)                                                                                                           
        try:                                                                                                                         
            return float(el)                                                                                                         
        except ValueError:                                                                                                           
            return el                                                                                                                

    value = loader.construct_scalar(node)                                                                                            
    # remove the ( ) from the string                                                                                                 
    tup_elements = value[1:-1].split(',')                                                                                            
    # remove the last element if the Tuple was written as (x,b,)                                                                     
    if tup_elements[-1] == '':                                                                                                       
        tup_elements.pop(-1)                                                                                                         
    tup = Tuple(map(parse_tup_el, tup_elements))                                                                                     
    return tup                                                                                                                       

# !Tuple is my own tag name, I think you could choose anything you want                                                                                                                                   
yaml.add_constructor(u'!Tuple', yml_Tuple_constructor)
# this is to spot the strings written as Tuple in the yaml                                                                               
yaml.add_implicit_resolver(u'!Tuple', re.compile(r"\(([^,\W]{,},){,}[^,\W]*\)")) 

Enfin en exécutant ceci:

>>> yml = yaml.load("""
   ...: cities:
   ...:   1: (0,0)
   ...:   2: (4,0)
   ...:   3: (0,4)
   ...:   4: (4,4)
   ...:   5: (2,2)
   ...:   6: (6,2)
   ...: highways:
   ...:   - (1,2)
   ...:   - (1,3)
   ...:   - (1,5)
   ...:   - (2,4)
   ...:   - (3,4)
   ...:   - (5,4)
   ...: start: 1
   ...: end: 4""")
>>>  yml['cities']
{1: (0, 0), 2: (4, 0), 3: (0, 4), 4: (4, 4), 5: (2, 2), 6: (6, 2)}
>>> yml['highways']
[(1, 2), (1, 3), (1, 5), (2, 4), (3, 4), (5, 4)]

Il pourrait y avoir un inconvénient potentiel avec save_load Par rapport à load que je n'ai pas testé.

3
Olivier