PyYAMLでカスタムタグを使う


調べてみたら全くといっていいほどサンプルが(英語圏にも!)なかったので簡単なのを

きっかけ

YAMLでプログラムの挙動を定義するコードをPythonで書いていて、ちょっとYAML内にオブジェクトを生成する処理を書きたくなりました。

PyYAMLDocumentationを見る限りタグというものがあって、どうもそれが目的のことをやってくれそうなのですが、どこにも利用例がなくどう使えばいいのか、どう定義すればいいのかが分かりません。さて困った。

タグを追加する方法

で、いきなり本題です。タグを追加するには、PyYAMLモジュールのグローバル関数、add_multi_constructor()というものに値を渡せば良い。

  yaml.add_multi_constructor("tag:yaml.org,2002:var", var_handler)

  # ...

  def var_handler(loader, suffix, node=None):
    # 処理

こういうコードを書くと、YAMLファイルに以下のようなタグがあったときに反応し、var_handlerが呼び出される。

  textvariable: !!var:StringVar
    name: label
    default: ""

引数に指定した関数には三つのパラメータが引き渡され

  • loader:読み込み中のYAMLファイルを処理しているローダーオブジェクトそのもの
  • suffix:タグプレフィックスの後ろに続く文字列。上記YAMLコードの場合、:StringVar
  • node:タグを発見した箇所を示すノードオブジェクト。valueにはここより下の下位層にあるノードが格納される。 上記YAMLコードの場合、MappingNode(value=[(ScalarNode(), ScalarNode()), (ScalarNode(), ScalarNode())])

で、関数の戻り値としてなんらかの値をリターンすると、タグの部分が戻り値に指定した値になる。
たとえば上記例の場合、「Yes」という文字を返すと、textvariable: Yesと書いたのと同じようになる。

add_multi_constructor()関数の挙動を追ってみると、キーワード引数として定義されているLoaderを省略した場合、ここで指定した関数はLoader, FullLoader, UnsafeLoaderの三つのクラスに追加されるようです。なので、SafeLoaderを使った場合は、ここで何をしたとしても関数は追加されません。

YAMLで独自に定義したタグだけを使いたい場合

YAMLを読み込むとき、独自で定義したタグのみを読み込みたい場合は、SafeLoaderを引数で指定して関数を追加すると良いです。

わたしは別の処理もあったのと、SafeLoaderを直接いじってしまうことに一抹の不安を感じたため、SafeLoaderを継承した新しいクラスを定義してしまいました。

class GeneratorLoader(yaml.SafeLoader):
  def __init__(self, stream):
    super().__init__(stream)
    yaml.add_multi_constructor("tag:yaml.org,2002:var", GeneratorLoader.var_handler,
      Loader=GeneratorLoader)

  @staticmethod
  def var_handler(loader, suffix, node=None):
    pass

ここで作ったクラスをyaml.load()メソッドでLoaderとして使用すれば、タグが処理されるようになります。

PyYAMLのグローバル関数には、Loaderのインスタンスを受け取る関数がない

なお、yaml.load()などPyYAMLの__init__.pyに定義されている関数には、Loaderのインスタンスを受け取る関数がありません(すべてクラスを受け取っています)。
このため、上記で作成したLoaderオブジェクトを、YAML読み込み後に使用したい場合は、Loader#get_single_data()を直接呼び出す必要があります。

loader = GeneratorLoader(self.string)
struct = loader.get_single_data()

その他の注意点

  • タグは必ず「tag:yaml.org,2002」ではじまる必要があります。よってadd_multi_constructor()関数の第一引数の値は必ず「tag:yaml.org,2002」からはじまります。ただし、この部分は実際にYAMLには書きません(「tag:yaml.org,2002:var」というタグを定義した場合、YAMLに書くのは「var」のみとなります)

説明がよくわからないからとりあえずコードを見せろ

現在作りかけですが次のモジュールで使用しています。

2020/09/21現在はtksugar/generator.pyに当該処理があります(あとでコードを移動させるかも)。

参考文献