nikkie-ftnextの日記

イベントレポートや読書メモを発信

Python 標準ライブラリの ast.NodeVisitor、こんなに短い実装で高い拡張性を実現していたんですね👏

はじめに

七尾百合子さん、お誕生日 335日目 おめでとうございます! nikkieです。

抽象構文木の話です。
ast.NodeVisitorについて発見がありました。

目次

nikkie とast.NodeVisitor

NodeVisitorには大変お世話になっております。
継承して田中琴葉ちゃんを作り出すほどです

class ArgumentConcreteTypeHintChecker(ast.NodeVisitor):
    def visit_arg(self, node):
        # 省略

こちらはNodeVisitorを継承したNodeTransformerを私の方で継承した例

class StrNodeTransformer(ast.NodeTransformer):
    def visit_Constant(self, node: ast.Constant):
        # 省略

GoFデザインパターンの Visitor ですね

このたび改めてドキュメントを読みました。

ast.NodeVisitorのドキュメント

https://docs.python.org/ja/3/library/ast.html#ast.NodeVisitor

抽象構文木を渡り歩いてビジター関数を見つけたノードごとに呼び出すノード・ビジターの基底クラスです。

2つのメソッドを持ちます

  • visit(node)
    • デフォルトの実装では self.visit_*classname* というメソッド (ここで classname はノードのクラス名です) を呼び出すか、そのメソッドがなければ generic_visit() を呼び出します。

  • generic_visit(node)
    • このビジターはノードの全ての子について visit() を呼び出します。

ここを確認して、上述の例でvisit_argvisit_Constantを定義した理由が今までよりも分かりました。

  • ArgumentConcreteTypeHintChecker().visit(tree)
  • -> Module node についてvisit_Moduleはないので、generic_visit()呼び出し
  • -> treeの子についてvisit()呼び出し

再帰的に子を呼び出していき、arg node に至ったら、私が実装したvisit_arg()が呼び出されます!
visit_arg()では最後にgeneric_visit()を呼んでいるので、子についてvisit()呼び出しに戻るのですね

class ArgumentConcreteTypeHintChecker(ast.NodeVisitor):
    def visit_arg(self, node):
        # 省略
        self.generic_visit(node)

さらに実装を覗いたところ、思ったよりも少ない行数で実装されているという発見がありました。

ast.NodeVisitorの実装

https://github.com/python/cpython/blob/v3.14.3/Lib/ast.py#L482-L516

class NodeVisitor(object):
    def visit(self, node):
        method = 'visit_' + node.__class__.__name__
        visitor = getattr(self, method, self.generic_visit)
        return visitor(node)

    def generic_visit(self, node):
        for field, value in iter_fields(node):
            if isinstance(value, list):
                for item in value:
                    if isinstance(item, AST):
                        self.visit(item)
            elif isinstance(value, AST):
                self.visit(value)

ドキュメントで確認したとおりです!

  • visit()visitor = getattr(self, method, self.generic_visit)がまさにそれ
    • node 向けのvisit_*classname*が見つからなければgeneric_visit()を呼び出します
    • 動的型付けの Python らしい実装だと思いました
  • generic_visit()はフィールドのそれぞれ(子と理解)に対してvisit()再帰的に呼び出しています

以前 pyflakes の visitor の実装を見ていて、「めちゃ長く書くんだな」と思っていたので、NodeVisitorの実装が短かったのは驚きでした

終わりに

ast.NodeVisitorのドキュメントや実装を見て、今まで書いていた実装の理解が深まりました。

  • visit()visit_*classname*を呼び出すが、見つからなければgeneric_visit()を呼び出す
    • だからNodeVisitorを継承したクラスでは、visit_*classname*を実装する必要があった
  • generic_visit()で AST のノードの子に対してvisit()を呼び出す(再帰
    • NodeVisitorを継承したクラスのvisit_*classname*では、generic_visit(node)を呼び出して再帰的な処理とする必要があった

実装を見てドキュメントの理解も深まりました。

rustpython-ast の visitor とは少し違うのも面白いですね。
visit_*classname*generic_visit_*classname*がある)