Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
43
Help us understand the problem. What are the problem?

posted at

updated at

Organization

たった1行から始めるPythonのAST(抽象構文木)入門

はじめに

この記事は2020年のRevCommアドベントカレンダー20日目の記事です。 19日目は@metal-presidentさんの「モバイルチームの成長とKMM導入に向けて」でした。

11月に株式会社RevCommに入社した@rhoboroです。
前職では主にGCP x Pythonで、現職では主にAWS x Pythonで日々業務を行なっています。
RevCommでは広島県の尾道からフルリモートワークで働いているので、そういった働き方にもし興味があればこちらの記事もご覧ください。

それでは、本題に入ります。

PythonのAST(抽象構文木)とは?

この記事は、PythonのAST(抽象構文木、Astract Syntax Tree)に触れたことのない方を対象にしたASTの入門記事です。
そもそもASTとは何なのか、ASTを理解すると何ができるのかを中心に紹介していきます。

さっそくですが、タイトルにもある通りまずは1行のコマンドを打ってみましょう。
次のモジュールschema.pyを用意してから、その下にあるpython3コマンドを実行してください。

schema.py
# このクラスは下記にありました
# https://docs.python.org/ja/3/tutorial/classes.html
class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'
$ python3 -c 'import ast; print(ast.dump(ast.parse(open("schema.py").read()), indent=4))'

コマンドを実行すると、次のように出力されます。(この結果はPython3.9で実行したものです)
先ほどのschema.pyとよく見比べてみると、見た目は違いますがなんとなくソースコードと同じものを表現していることがわかると思います。また、ModuleClassDefExprなどがPythonのクラス名だとすると、この結果はPythonのオブジェクトにも見えてきます。

Module(
    body=[
        ClassDef(
            name='MyClass',
            bases=[],
            keywords=[],
            body=[
                Expr(
                    value=Constant(value='A simple example class')),
                Assign(
                    targets=[
                        Name(id='i', ctx=Store())],
                    value=Constant(value=12345)),
                FunctionDef(
                    name='f',
                    args=arguments(
                        posonlyargs=[],
                        args=[
                            arg(arg='self')],
                        kwonlyargs=[],
                        kw_defaults=[],
                        defaults=[]),
                    body=[
                        Return(
                            value=Constant(value='hello world'))],
                    decorator_list=[])],
            decorator_list=[])],
    type_ignores=[])

もうお気づきだと思いますが、これこそがPythonのASTオブジェクトです。このようにAST(抽象構文木)とは、文字列であるソースコードを解析し、それを木構造で表現したものです。
つまり、Pythonがプログラムを実行する際には、次のような処理が動いてます。

  1. ソースコードを解析してASTオブジェクトが生成される
  2. ASTオブジェクトからコードオブジェクトが生成される
  3. コードオブジェクトから実行可能なバイトコードが生成され、実行される

PythonのASTの見方

ここまででASTとはソースコードと実行可能なバイトコードの中間表現であることは何となく理解できたと思います。
それではもう少しASTオブジェクトの中を見ていきましょう。まずはそのために必要となる道具の紹介です。

標準ライブラリのastモジュール

Pythonの標準ライブラリには、ASTオブジェクトを扱うのに便利なastモジュールがあります。
先ほど実行したコマンドでも、次の2つのヘルパー関数を利用していました。
ここではどちらも一言で説明していますので、詳細は公式ドキュメントのリンクを見てください。

$ python3 -c 'import ast; print(ast.dump(ast.parse(open("schema.py").read()), indent=4))'
  • ast.parse(): 渡されたソースを解析してASTオブジェクトを返します
  • ast.dump(): 渡されたASTオブジェクトの木構造を見やすくダンプします

また、先ほどのコマンド出力結果にあったClassDefExprAssignといったキーワードはすべてast.ASTクラスのサブクラスです。定義されているサブクラスの一覧は公式ドキュメントの抽象文法を見るとわかります。抽象文法の左辺のシンボルひとつずつにクラスがあり、右辺にあるコンストラクタはそれぞれ左辺のシンボルのサブクラスです。

ASTオブジェクトを読み解く

これで必要なものが揃ったので実際にASTを見ていきましょう。
ただし、先ほどの出力結果だと大きすぎるので、ここではx=1というとてもシンプルなPythonのソースコードのASTオブジェクトを見ていきます。

$ python3 -c 'import ast; print(ast.dump(ast.parse("x=1"), indent=4))'
Module(
    body=[
        Assign(
            targets=[
                Name(id='x', ctx=Store())],
            value=Constant(value=1))],
    type_ignores=[])

Moduleは先ほどもあったのでここでは無視すると、x=1を表現しているのはAssignのところです。

Assign(
    targets=[
        Name(id="x", ctx=Store())
    ],
    value=Constant(value=1)
)

Assignは名前からわかる通り代入(assignment)を表現するノードです。
代入の左辺にあたるものがtargetsに、右辺にあたるものがvalueにそれぞれ格納されています。1

したがって、代入の左辺xを表現しているノードはName(id="x", ctx=Store())だとわかります。
Nameの引数ctxは、変数の格納、読み込み、削除と対応していて、それぞれStore()Load()Del()となっています。右辺1は定数なのでそのままConstant(value=1)ですね。

これでこのASTオブジェクトがx=1という式を表していることが理解できたと思います。この記事の最初のコマンド結果のASTオブジェクトも、同じようにastモジュールのドキュメントを片手にひとつずつ見ていくと読み解けるでしょう。

ASTオブジェクトの活用

ASTオブジェクトは先ほども述べたようにソースコードと実行可能なバイトコードの中間表現です。
それでいてPythonオブジェクトでもあるため、ソースコードやコードオブジェクトよりもPythonのプログラムから処理しやすいです。そのため、ASTオブジェクトは様々な活用方法があります。

例をあげるとmypyflake8といった静的解析ツールなどで利用されていたり、pytestではassert文のASTオブジェクトを変更しassert文をより便利なものにしています。そのほかにも、通常のPythonのソースコードではないファイルからASTオブジェクトを生成してPythonのオブジェクトとして動かすこともできます。2

また、Python3.9で追加されたast.unparse()を使うと、ASTオブジェクトからソースコードを生成できます。これを利用してJSONファイルからASTオブジェクトを構築し、pydanticのモデルクラスを生成するライブラリpydantic-generatorを作成しました。もしよかったら触ってみてください。

最後に注意

ASTオブジェクトの変更はユーザーや他の開発者の思いもしない挙動となり、混乱を生じさせる可能性が高いです。
それ以外の方法がないというとき以外は使わないようにしましょう。3

おわりに

わたし自身もそうでしたが、ASTは難しいという印象を持っている方も多いのではないでしょうか。
しかし、蓋を開けてみればドキュメントも1ページだけですし、ソースコードと1対1で対応しているためとてもシンプルなものです。
便利なこの1行でいろんなモジュールのASTオブジェクトを眺めてみてください。

$ python3 -c 'import ast; print(ast.dump(ast.parse(open("YourFile.py").read()), indent=4))'

明日はリサーチチームの@k_ishiさんです。
2020年のRevComm Advent Calendarは一日も途切れることなく続いてますので、明日もお楽しみに!


  1. 左辺がリストになっているのは、a, b = (1, 2)のようなアンパックのためです。 

  2. https://ja.wikipedia.org/wiki/Hy 

  3. 本当に必要な人は、それが必要だと理解している人です。必要か迷った時はおそらく必要ありません。 

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
43
Help us understand the problem. What are the problem?