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に指示する方法はありますか?
私があなたがやろうとしていることをハックしたことを私は呼ぶつもりはありません。私の理解からの別のアプローチは、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)]}
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でも同じことができるはずです。
私は質問と同じ問題を抱えていましたが、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
_には潜在的な欠点がある可能性があります。