連載
» 2024年01月16日 05時00分 公開

[解決!Python]PyYAMLモジュールを使ってオブジェクトをYAMLドキュメントとして書き出すには解決!Python

PythonのオブジェクトをPyYAMLモジュールによってYAMLドキュメントへシリアライズする方法と、各種オブジェクトがどのようにシリアライズされるかを紹介する。

[かわさきしんじDeep Insider編集部]
「解決!Python」のインデックス

連載目次

import yaml
from yaml import dump, dump_all, safe_dump, safe_dump_all
from yaml import safe_load, full_load, unsafe_load
from pathlib import Path  # ファイルの内容の確認用

# 文字列に出力
lst = [0, 1, 2]
result = dump(lst)  # 文字列に出力
print(result)
# 出力結果:
#- 0
#- 1
#- 2
#

# ファイルに出力
with open('sample.yaml', 'w') as f:
    dump(lst, f)  # ファイルに出力
print(Path('sample.yaml').read_text())
# 出力結果:
#- 0
#- 1
#- 2
#

# 複数のYAMLドキュメントを出力
lst = [['foo', 'bar', 'baz'], [0, 1, 2]]
result = dump_all(lst)
print(result)
# 出力結果(「---」はYAMLドキュメントの開始を示す):
#- foo
#- bar
#- baz
#---
#- 0
#- 1
#- 2

result = dump_all(lst, explicit_start=True, explicit_end=True)
print(result)
# 出力結果(「---」はYAMLドキュメントの開始を、「...」は終了を示す):
#---
#- foo
#- bar
#- baz
#...
#---
#- 0
#- 1
#- 2
#...

# リスト
l = ['foo', 'bar', 1, 2, 3]
result = dump(l)
print(result)
# 出力結果:
#- foo
#- bar
#- 1
#- 2
#- 3

# タプル
t = ('kawasaki', 170, 65.0)
result = dump(t)
print(result)
# 出力結果:
#!!python/tuple
#- kawasaki
#- 170
#- 65.0
tmp = safe_load(result)  # yaml.constructor.ConstructorError例外
tmp = full_load(result)
print(tmp)  # ('kawasaki', 170, 65.0)

result = safe_dump(t)  # safe_dump関数が生成するタグはYAML標準のものだけ
print(result)
# 出力結果:
#- kawasaki
#- 170
#- 65.0

# 辞書
d = {'a': 0, 'b': 1}
result = dump(d)
print(result)
# 出力結果:
#a: 0
#b: 1

# 集合
s = {'a', 'b', 'c'}
result = dump(s)
print(result)
# 出力結果:
#!!set
#a: null
#b: null
#c: null

result2 = safe_dump(s)
print(result2)
# 出力結果:
#!!set
#a: null
#b: null
#c: null

# 辞書のリスト
lst = [{'a': 0, 'b': 1}, {'x': 'foo', 'y': 'bar'}]
result = dump(lst)
print(result)
# 出力結果:
#- a: 0
#  b: 1
#- x: foo
#  y: bar

# 辞書を値とする辞書
d = {'foo': {'a': 0, 'b': 1}, 'bar': {'x': 'X', 'y': 'Y'}}
result = dump(d)
print(result)
# 出力結果:
#bar:
#  x: X
#  y: Y
#foo:
#  a: 0
#  b: 1

# ユーザー定義クラスのインスタンスを出力
class Foo(yaml.YAMLObject):
    yaml_tag = '!Foo'
    def __init__(self, x, y):
        self.x = x
        self.y = y

f = Foo(10, 20)
result = dump(f)
print(result)
# 出力結果:
#!Foo
#x: 10
#y: 20

f1 = safe_load(result)  # yaml.constructor.ConstructorError例外
f1 = full_load(result)  # OK
f1 = unsafe_load(result)  # OK

# フロースタイルで出力
lst = [0, 1, 2]
result = dump(lst, default_flow_style=True)
print(result)  # [0, 1, 2]

t = ('kawasaki', 170, 65.0)
result = dump(t, default_flow_style=True)
print(result)  # !!python/tuple [kawasaki, 170, 65.0]

d = {'a': 0, 'b': 1}
result = dump(d, default_flow_style=True)
print(result)  # {a: 0, b: 1}

lst = [{'a': 0, 'b': 1}, {'x': 'foo', 'y': 'bar'}]
result = dump(lst, default_flow_style=True)
print(result)  # [{a: 0, b: 1}, {x: foo, y: bar}]


PyYAMLモジュールを使ったYAMLドキュメントの書き出し

 YAML(YAML Ain't Markup Language)はオブジェクトやデータをシリアライズするための言語仕様であり、可読性が高いことが特徴となっている。「PyYAMLモジュールを使ってYAMLデータを読み込むには」ではYAMLドキュメントを読み込む方法を紹介したが、今回はPyYAMLモジュールを使ってPythonのオブジェクトをYAMLドキュメントとして書き出す方法を紹介する。PyYAMLはPythonには標準で添付されていないので「pip install pyyaml」「py -m pip install pyyaml」などの方法で事前にインストールしておく必要がある。

 PythonのオブジェクトをYAMLドキュメントとして書き出すには、PyYAMLモジュールが提供する以下の関数を使える。

  • dump関数:オブジェクトを単一のYAMLドキュメントとして書き出す
  • dump_all関数:リストやジェネレーターを受け取り、その要素の一つ一つを独立したYAMLドキュメントとして書き出す
  • safe_dump関数:Pythonに固有のオブジェクト(タプルなど)を受け取った場合に、それをYAMLが標準で提供するタグを付けて書き出す。復元時にPython固有のオブジェクトとして復元されない可能性があるが、多言語間での移植性のあるYAMLドキュメントが生成できる
  • safe_dump_all関数:Pythonのリストやジェネレーターの要素の一つ一つを独立したYAMLドキュメントとして書き出す。その際の挙動はsafe_dump関数と同様

 ここでは主にdump関数を使ってコード例を示していく。

YAMLドキュメントとして書き出す基本型

 dump関数(やその他の関数)は文字列あるいはストリーム(ファイルなど)への書き出しをサポートしている。最も簡単な使い方を以下に示す。

import yaml
from yaml import dump, dump_all, safe_dump, safe_dump_all
from yaml import safe_load, full_load, unsafe_load
from pathlib import Path  # ファイルの内容の確認用

lst = [0, 1, 2]
result = dump(lst)  # 文字列に出力
print(result)
# 出力結果:
#- 0
#- 1
#- 2
#


 dump関数にファイルストリームなどを与えずに、YAMLドキュメントとして書き出したいオブジェクトだけを与えると、YAMLドキュメント化された文字列が返される。上のコード例では、リストを与えているが、それがYAMLのブロックシーケンスに変換されたものが返されている。

 ファイルに書き出すにはdump関数のstreamパラメーターにファイルストリームを指定する。以下に例を示す。

with open('sample.yaml', 'w') as f:
    dump(lst, f)  # ファイルに出力
print(Path('sample.yaml').read_text())
# 出力結果:
#- 0
#- 1
#- 2
#


 ファイルに出力されたことを除けば最初の例と同じだ。

 YAMLでは単独のファイルや文字列に複数のYAMLドキュメントを含められる。PyYAMLでそのようなファイルまたは文字列を作成するにはdump_all関数(またはsafe_dump_all関数)を使用する。この関数はリストやジェネレーターを受け取ると、その一つ一つの要素がYAMLドキュメントとして書き出される。以下に例を示す。

lst = [['foo', 'bar', 'baz'], [0, 1, 2]]
result = dump_all(lst)
print(result)
# 出力結果(「---」はYAMLドキュメントの開始を示す):
#- foo
#- bar
#- baz
#---
#- 0
#- 1
#- 2


 この例では、['foo', 'bar', 'baz']というリストと[0, 1, 2]というリストを要素とするリストをdump_all関数に渡している。dump_all関数は外側のリストの要素である2つのリストを独立したYAMLドキュメント(ブロックシーケンス)として出力している。YAMLドキュメントのドキュメントは「---」で始まるので、真ん中にこれが記述されている。

 このときには、explicit_startパラメーターとexplicit_endパラメーターにTrueを指定することで、YAMLドキュメントの開始を「---」で、終了を「...」で明示的に記述することも可能だ。以下に例を示す。

result = dump_all(lst, explicit_start=True, explicit_end=True)
print(result)
# 出力結果(「---」はYAMLドキュメントの開始を、「...」は終了を示す):
#---
#- foo
#- bar
#- baz
#...
#---
#- 0
#- 1
#- 2
#...


Pythonのオブジェクトがどうシリアライズされるか

 次にPythonの代表的なオブジェクトがどのようにYAMLドキュメントにシリアライズされるかを簡単にまとめる。

 リストは先ほども見たように、特に指定をしなければブロックシーケンスに変換される。

l = ['foo', 'bar', 1, 2, 3]
result = dump(l)
print(result)
# 出力結果:
#- foo
#- bar
#- 1
#- 2
#- 3


 これに対して、タプルは「!!python/tuple」というPython固有のYAMLタグ付きでYAMLドキュメントに変換される。

t = ('kawasaki', 170, 65.0)
result = dump(t)
print(result)
# 出力結果:
#!!python/tuple
#- kawasaki
#- 170
#- 65.0


 注意したいのは、Python固有のタグ付きで書き出されたYAMLドキュメントはPyYAMLのsafe_load関数では読み込めない点だ。その場合にはfull_load関数かunsafe_load関数で読み込む必要がある。以下に例を示す。

tmp = safe_load(result)  # yaml.constructor.ConstructorError例外
tmp = full_load(result)
print(tmp)  # ('kawasaki', 170, 65.0)


 Python固有のオブジェクトとして書き出すのではなく、YAMLで規定されているデータ構造として書き出すのであれば、safe_dump関数を使用する。以下はその例だ。

result = safe_dump(t)  # safe_dump関数が生成するタグはYAML標準のものだけ
print(result)
# 出力結果:
#- kawasaki
#- 170
#- 65.0


 この場合は、タプルはリストと同様に、ブロックシーケンスに変換される。そのため、このYAMLドキュメントはsafe_load関数でも読み込み可能だ。ただし、読み込んだ後はタプルではなく、リストに復元されることには注意しよう。

 辞書オブジェクトは以下のようにYAMLのブロックマッピングに変換される。

d = {'a': 0, 'b': 1}
result = dump(d)
print(result)
# 出力結果:
#a: 0
#b: 1


 集合オブジェクトは「!!set」というYAMLの標準タグ付きで、値を持たないマッピングに変換される。

s = {'a', 'b', 'c'}
result = dump(s)
print(result)
# 出力結果:
#!!set
#a: null
#b: null
#c: null


 これはYAMLの標準タグなので、safe_load関数でも読み込めるはずだ(safe_dump関数で書き出しても、「!!set」タグは付加される)。

result2 = safe_dump(s)
print(result2)
# 出力結果:
#!!set
#a: null
#b: null
#c: null


 少し複雑なオブジェクトについても見てみよう。辞書を要素とするリストを変換する例を以下に示す。

lst = [{'a': 0, 'b': 1}, {'x': 'foo', 'y': 'bar'}]
result = dump(lst)
print(result)
# 出力結果:
#- a: 0
#  b: 1
#- x: foo
#  y: bar


 出力結果を見ると、辞書を要素とするリストは、ブロックマッピングを要素とするブロックシーケンスに変換されたことが分かる。

 次に、辞書を値とする辞書についても見てみる。

d = {'foo': {'a': 0, 'b': 1}, 'bar': {'x': 'X', 'y': 'Y'}}
result = dump(d)
print(result)
# 出力結果:
#bar:
#  x: X
#  y: Y
#foo:
#  a: 0
#  b: 1


 この場合はブロックマッピングを要素とするブロックマッピングに変換されている。

 なお、dump関数はユーザー定義クラスのインスタンスをシリアライズすることも可能だ。以下に例を示す。ポイントはユーザー定義クラスがyaml.YAMLObjectクラスを継承していることと、クラス変数yaml_tagに対応するタグを記述している点だ。

class Foo(yaml.YAMLObject):
    yaml_tag = '!Foo'
    def __init__(self, x, y):
        self.x = x
        self.y = y

f = Foo(10, 20)
result = dump(f)
print(result)
# 出力結果:
#!Foo
#x: 10
#y: 20


 このようにすることで、クラス変数yaml_tagで示された「!Foo」タグ付きでYAMLドキュメントが出力される。ただし、このYAMLドキュメントはsafe_load関数で復元できず、復元するにはfull_load関数かunsafe_load関数を使う必要があるようだ。

f1 = safe_load(result)  # yaml.constructor.ConstructorError例外
f1 = full_load(result)  # OK
f1 = unsafe_load(result)  # OK


フロースタイルでの出力

 ここまでは、dump関数の出力はブロックシーケンスやブロックマッピングが使われていた。しかし、dump関数のdefault_flow_styleパラメーターにTrueを指定することで、フロースタイルの出力も可能だ。以下にまとめて例を示す。

lst = [0, 1, 2]
result = dump(lst, default_flow_style=True)
print(result)  # [0, 1, 2]

t = ('kawasaki', 170, 65.0)
result = dump(t, default_flow_style=True)
print(result)  # !!python/tuple [kawasaki, 170, 65.0]

d = {'a': 0, 'b': 1}
result = dump(d, default_flow_style=True)
print(result)  # {a: 0, b: 1}

lst = [{'a': 0, 'b': 1}, {'x': 'foo', 'y': 'bar'}]
result = dump(lst, default_flow_style=True)
print(result)  # [{a: 0, b: 1}, {x: foo, y: bar}]


 こちらのスタイルの方が読みやすくて好きという場合には、このパラメーターをTrueにすることをオススメする。

「解決!Python」のインデックス

解決!Python

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。