スクリプトをノードにできたので次に進みます。Rubyだとこの後バイトコードに変換していましたがPythonはノードをさらにASTというものに変換するようです。その処理は見出しに書いたようにPython/ast.cに書かれています。
AST変換のトップレベル関数であるPyAST_FromNodeObjectではノードがファイルを読んだものなのか、evalなのか、シェルで打ち込んだものなのかで場合分けしていますが基本的な流れは同じようです。今回のケースについて考えていると、
single_input simple_stmt small_stmt expr_stmt (以下略)
以下のコードが実行されることがわかります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | - - ! - - | | ! - - ! | ! | |
|
simple_stmtを引数にしてast_for_stmtが呼び出されます。
ここからはノードに対応するast_*関数を使って変換が行われていきます。今回の場合、
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| - - | | ! - | | ! - | - | ! - | | |
|
と、ast_for_expr_stmtに処理が回されます。なお、
と徐々にnが指すものが変わっていっているので注意してください。
関数はast_for_expr_stmt、ast_for_testlist、ast_for_exprと進んでいきます。ast_for_exprは少し長めでexprが子ノード1つだけの場合にどんどん子ノードに進んでいきます。
ここまでの知識を使って前回構築したノードをもう少しシンプルにしてみましょう。今回の場合、n * n、○ > 10以外の部分以外は全部子ノードが1つです。
single_input expr_stmt power atom_expr atom testlist_comp power atom_expr atom NAME(n) comp_for exprlist power atom_expr atom NAME(n) power atom_expr atom NAME(range) trailer arglist argument power atom_expr atom NUMBER(10) comp_iter comp_if comparison term power atom_expr atom NAME(n) * power atom_expr atom NAME(n) comp_op > power atom_expr atom NUMBER(10)
だいぶシンプルになりました。多分ここもシンプルになるというところもありますがいったん先に進んでみましょう。comparisonやtermについては後で見ます。
処理はast_for_power、ast_for_atom_expr、ast_for_atomと進んでいきます。atom_exprはメソッド呼び出しの処理をしていますがそれは後で見ます。消してもいい(子ノードが1つの)power、atom_expr、およびatomを削除すると、
single_input expr_stmt testlist_comp NAME(n) comp_for exprlist NAME(n) atom_expr NAME(range) trailer arglist argument NUMBER(10) comp_iter comp_if comparison term NAME(n) * NAME(n) comp_op > NUMBER(10)
となります。一画面に収まるようになりました。
さて、実はast_for_atomを見ていると'['などもノードの子として格納されていることがわかったのですがそれをノードに書くとまた長くなるのでここでは書かないことにします。ともかく今回の場合はast_for_listcompに処理が進みます。
ast_for_listcompはast_for_itercompを呼び出すだけです。エラー処理とかを削ったast_for_itercompは以下のような感じ。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| - - ! | | | | | | | | | | | | | | | ! |
|
子ノードの1つ目に対してast_for_exprが呼びだされています。今回の場合は単純に「n」なのでast_for_atomまで行ってNameが返されます。
その後、ast_for_comprehensionが呼ばれています。この部分は名前通り内包表記、つまり今回の肝です。
ast_for_comprephensionはforやifが何回も書けるのに対応するために長くなっていますがやってることはそんなに難しくありません。エラー処理省いて今回通るとこだけ抜き出すと、
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| - | | | | | - | | | | | | | | | | | | | - | | | | | | - | | | ! | ! | ! | ! |
|
n_forsもn_ifsも1です。というわけでforの直後のexprlist、inの直後のexpr、ifの部分のexprが処理されます。
exprlistは名前の通り、exprをリスト処理しています。contextというものを設定していますがまあ実行コンテキストだろうということで深くは追及しません。
次、inの後のexpr、具体的にはrange(10)、関数呼び出しです。ノードで言うと以下の部分です。
atom_expr NAME(range) trailer arglist argument NUMBER(10)
ast_for_atom_exprに来た時に今度はast_for_trailerが呼び出されます。ast_for_trailerでは関数呼び出し、配列参照、属性参照が処理されます。今回は関数呼び出しなので、
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| - - | | ! | - | | | | | ! ! |
|
が処理されます。なお、NCH(n)が2になるのは引数なしの場合なので今回はast_for_callに続きます。
ast_for_callも結構長いです。まあ関数呼び出しはプログラムの肝ですからね。そういえばオブジェクトのメソッド呼び出しってどうなってるんだと気になりましたが、ASTの時点では属性(実態は関数)の関数呼び出しって階層構造になっているなってるみたいですね。どう実行されるか気になりますが今回は深く追いかけないことにします。
で、ast_for_call。arglistに普通の引数なのかキーワード引数なのかなどに応じて処理が行われます。今回は単純に普通の引数1個だけです。
これで「in range(10)」まで終わったので後はifの部分、「if n * n > 10」です。ノードだと以下の感じ、
comparison term NAME(n) * NAME(n) comp_op > NUMBER(10)
comparisonの処理がされているのはast_for_exprです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | - | | | | | | ! - | ! |
|
なんでループしているのかと思ったらそういえばPythonは「0 < x < 10」みたいなコードが書けるのでした。注意としては、先頭の子ノードが最後にast_for_exprにかけられる点ですね。
というわけで先頭の子ノード、termです。ast_for_binopで処理されます。まあ特に特筆することはないでしょう。
以上、ノード(CSTというらしいです)がASTに変換される様子を見てきました。ASTというのはコードを見てる途中にあったInteractiveやListCompなどです。それらを書き出すと以下のようになります。
Interactive Expr ListComp Name(n) Comprephension Name(n) Call Name(Range) Num(10) Compare BinOp Mult Name(n) Name(n) Gt Num(10)
非常にシンプルになりました。実際には単一の値ではなくシーケンス(要素数1)の部分もありますがまあいいでしょう。