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

# はじめに

この記事は[2020年のRevCommアドベントカレンダー](https://qiita.com/advent-calendar/2020/revcomm)20日目の記事です。 19日目は[@metal-president](https://qiita.com/metal-president)さんの「[モバイルチームの成長とKMM導入に向けて](https://qiita.com/metal-president/items/f24b2f16f957b07db570)」でした。

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

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

# 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'
```

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

コマンドを実行すると、次のように出力されます。（この結果はPython3.9で実行したものです）
先ほどのschema.pyとよく見比べてみると、見た目は違いますがなんとなくソースコードと同じものを表現していることがわかると思います。また、`Module`や`ClassDef`、`Expr`などがPythonのクラス名だとすると、この結果は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モジュール](https://docs.python.org/ja/3/library/ast.html)があります。
先ほど実行したコマンドでも、次の2つのヘルパー関数を利用していました。
ここではどちらも一言で説明していますので、詳細は公式ドキュメントのリンクを見てください。

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

- [ast.parse()](https://docs.python.org/ja/3/library/ast.html#ast.parse): 渡されたソースを解析してASTオブジェクトを返します
- [ast.dump()](https://docs.python.org/ja/3/library/ast.html#ast.dump): 渡されたASTオブジェクトの木構造を見やすくダンプします

また、先ほどのコマンド出力結果にあった`ClassDef`や`Expr`、`Assign`といったキーワードはすべて`ast.AST`クラスのサブクラスです。定義されているサブクラスの一覧は[公式ドキュメントの抽象文法](https://docs.python.org/ja/3/library/ast.html#abstract-grammar)を見るとわかります。抽象文法の左辺のシンボルひとつずつにクラスがあり、右辺にあるコンストラクタはそれぞれ左辺のシンボルのサブクラスです。

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

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

```shell
$ 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`のところです。

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

[Assign](https://docs.python.org/ja/3/library/ast.html#ast.Assign)は名前からわかる通り代入(assignment)を表現するノードです。
代入の左辺にあたるものが`targets`に、右辺にあたるものが`value`にそれぞれ格納されています。[^1]


したがって、代入の左辺xを表現しているノードは[Name(id="x", ctx=Store())](https://docs.python.org/ja/3/library/ast.html#ast.Name)だとわかります。
Nameの引数ctxは、変数の格納、読み込み、削除と対応していて、それぞれ`Store()`、`Load()`、`Del()`となっています。右辺1は定数なのでそのまま[Constant(value=1)](https://docs.python.org/ja/3/library/ast.html#ast.Constant)ですね。

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

# ASTオブジェクトの活用

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

例をあげると[mypy](https://github.com/python/mypy)や[flake8](https://github.com/PyCQA/flake8)といった静的解析ツールなどで利用されていたり、[pytest](https://github.com/pytest-dev/pytest)では[assert文のASTオブジェクトを変更](https://github.com/pytest-dev/pytest/blob/bd894e3065ba6fa13327ad5dfc94f2b6208cf0ff/src/_pytest/assertion/rewrite.py#L823)しassert文をより便利なものにしています。そのほかにも、通常のPythonのソースコードではないファイルからASTオブジェクトを生成してPythonのオブジェクトとして動かすこともできます。[^2]


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

### 最後に注意

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


# おわりに

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

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

明日はリサーチチームの[@k_ishi](https://qiita.com/k_ishi)さんです。
[2020年のRevComm Advent Calendar](https://qiita.com/advent-calendar/2020/revcomm)は一日も途切れることなく続いてますので、明日もお楽しみに！

[^1]: 左辺がリストになっているのは、`a, b = (1, 2)`のようなアンパックのためです。
[^2]: https://ja.wikipedia.org/wiki/Hy
[^3]: 本当に必要な人は、それが必要だと理解している人です。必要か迷った時はおそらく必要ありません。