はじめに
この記事は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コマンドを実行してください。
# このクラスは下記にありました
# 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とよく見比べてみると、見た目は違いますがなんとなくソースコードと同じものを表現していることがわかると思います。また、Module
やClassDef
、Expr
などが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がプログラムを実行する際には、次のような処理が動いてます。
- ソースコードを解析してASTオブジェクトが生成される
- ASTオブジェクトからコードオブジェクトが生成される
- コードオブジェクトから実行可能なバイトコードが生成され、実行される
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オブジェクトの木構造を見やすくダンプします
また、先ほどのコマンド出力結果にあったClassDef
やExpr
、Assign
といったキーワードはすべて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オブジェクトは様々な活用方法があります。
例をあげるとmypyやflake8といった静的解析ツールなどで利用されていたり、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は一日も途切れることなく続いてますので、明日もお楽しみに!