web-dev-qa-db-ja.com

PyYAMLを使用してpythonタプルを読み取る方法は?

input.yamlという名前の次の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

PyYAMLを使用してそれをロードし、次のように結果を出力しています。

import yaml

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

print(data)

結果は次のデータ構造です。

{ '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
}

ご覧のとおり、各都市と高速道路はリストとして表されています。ただし、タプルとして表現してほしい。したがって、内包表記を使用して手動でタプルに変換します。

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)

しかし、これはハックのようです。リストではなくタプルとして直接読み取るようにPyYAMLに指示する方法はありますか?

20
Aadit M Shah

私があなたがやろうとしていることをハックしたことを私は呼ぶつもりはありません。私の理解からの別のアプローチは、YAMLファイルでPython固有のタグを使用して、yamlファイルをロードするときに適切に表されるようにすることです。ただし、これにはyamlファイルを変更する必要があります。巨大な場合、おそらくかなり苛立たしく、理想的ではありません。

これをさらに説明する PyYaml doc を見てください。最終的には!!python/Tupleそのように表現したい構造の前。サンプルデータを取得するには、次のようにします。

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

サンプルコード:

import yaml

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

print(d)

どちらが出力されます:

{'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

YAML入力が「ハック」からどこに来るかに応じて、特に安全でないyaml.safe_load()の代わりにyaml.load()を使用する場合は、良い解決策です。 YAMLファイルの「リーフ」シーケンスのみがタプルである必要がある場合は、次のようにすることができます¹:

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)

印刷する:

{'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}

その後、シーケンスを再び「通常の」リストにする必要がある場合に、より多くの素材を処理する必要がある場合は、次を使用します。

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

¹ これは ruamel.yaml YAML 1.2パーサーを使用して行われました。 YAML 1.1のみをサポートする必要がある場合や、何らかの理由でアップグレードできない場合は、古いPyYAMLでも同じことができるはずです。

3
Anthon

私は質問と同じ問題を抱えていましたが、2つの答えには満足していませんでした。 pyyamlのドキュメントを閲覧していると、2つの興味深いメソッド_yaml.add_constructor_と_yaml.add_implicit_resolver_が見つかりました。

暗黙的なリゾルバーは、文字列を正規表現と照合することにより、すべてのエントリに_!!python/Tuple_のタグを付ける必要があるという問題を解決します。タプル構文も使用したかったので、リスト_Tuple: [10,120]_を書く代わりにTuple: (10,120)と書いて、タプルに変換しました。また、外部ライブラリをインストールしたくありませんでした。これがコードです:

_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]*\)")) 
_

最後にこれを実行することにより:

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

テストしなかったloadと比較して、_save_load_には潜在的な欠点がある可能性があります。

3
Olivier