PYEEK

Python が動くまで

書いた文字が、コンピュータの実行できる形になるまでを順に追います。 題材は短いプログラムひとつです。

こういうプログラムがあります。

n = 2
total = 3 + 4 * n

* はかけ算を表します。

= は等号ではなく、右側を計算して左の名前にしまえ、という指示です。

かけ算が先ですから、total には 11 が入ります。

人が読めば、これだけのことがわかります。

切り分けたもの

total = 3 + 4 * n は、人が見れば単語の集まりです。 しかし文字としては、こう並んでいるだけです。

total=3+4*n

どこが一語なのかは、どこにも書かれていません。

組み合わせたもの

切り分けても、まだ計算はできません。

3 + 4 * n を、二とおりの順番で計算してみてください。

並べ直したもの

組み合わせが決まっても、まだ足りません。

木が表しているのは、何と何が組かということだけです。 どこから手をつけるかは、どこにも書かれていません。

木のまま実行することもできますが、Python は速さのために、先に一列へ並べ直します。

ほどき方は、左の枝から先にたどります。

値は出会った順に出し、演算は両方の枝が済んだところで出します。

文が二つあるときは、書かれた順に並べます。

しまう だけは、右の枝から片付けます。 しまうものが手元に無ければ、しまえないからです。

そして左の枝の total は、値として出されることがありません。total にしまう という一つの命令の中に、しまい先の名前として入ります。

  1. LOAD_CONST22 を出す
  2. STORE_NAMEnn にしまう
  3. LOAD_CONST33 を出す
  4. LOAD_CONST44 を出す
  5. LOAD_NAMEnn の中身を出す
  6. BINARY_OP*かける
  7. BINARY_OP+たす
  8. STORE_NAMEtotaltotal にしまう

先頭の二つは、一行目の n = 2 から出たものです。 三つめの 3 は、+ の左の枝なので、4 * n より先に出されます。

出したものは、どこに出すのか

ここで、ひとつ引っかかるところがあります。

「3 を出す」と言われて、どこに出すのでしょうか。

Python は、値を積み上げる場所を持っています。 出したものはそこに積み、使うときは一番上から取ります。

まだ何も積まれていません

読んでいるのは、誰か

命令の列を上から読んで、一つずつ処理していく別のプログラムがあります。CPython といいます。

Python は言語の名前で、それを実際に動かすのがこの CPython です。 ここまで Python がやっていると書いてきたことは、正確にはこの CPython がやっています。

最初に見せた十一行は、その CPython の一部です。

+ を見つけたときに動き出す部分の、いちばん先頭にあたります。

そのうち五行は push という命令で、後で使うために、いま入っている値を 退避しているだけです。まだ何も始めていません。

始まっているのは、この一行からです。

48 8b 47 08mov 0x8(%rdi),%rax

渡された値から、その値の種類を取り出しています。 続く三行で、その種類が足し算のやり方を持っているかどうかを調べます。

まだ足していません。

足す相手が、どういう種類のものなのかを見ているところです。

なぜ、そんなことを調べるのか。+ は、足し算とは限らないからです。

'abc' + 'def'   →   'abcdef'

' で囲んだものは、計算されずそのままの文字として扱われます。 文字どうしを + でつなぐと、くっついて一つになります。 同じ + でも、数を足すのか、文字をつなぐのかで、やることがまるで違う。

では、どちらなのかを先に決めておけばよさそうなものです。ところが、決められません。

切り分けの段階では、字面しか見ていませんでした。

木を組む段階でも、n の中身は見ていません。

並べ直した BINARY_OP + という命令にも、何を足すのかは書かれていません。

n に何が入るかは、実行してみるまで決まらない。 だから CPython は、足すその瞬間に、自分で調べるしかありません。

十一命令かけて、まだ足していない。

その大半が、これから何をするか決めるための下ごしらえです。

同じ名前でも、止まった場所は違う

書いたものが受け付けられないとき、三つのどこかで止まっています。

total = 3 + 4 *
File "e1.py", line 1
    total = 3 + 4 *
                   ^
SyntaxError: invalid syntax

最後の行は「構文エラー。組み立てられません」という意味です。組み立てで止まった

切り分けは成功しています。* の右側に何も無いので、組を作れませんでした。^ の印が指しているのは、まさにその場所です。

total = 'abc
File "e2.py", line 1
    total = 'abc
            ^
SyntaxError: unterminated string literal (detected at line 1)

最後の行は「終わっていない文字列」という意味です。切り分けで止まった

一番長いかたまりを取ろうとして、閉じる ' を探しに行き、行の終わりまで見つかりませんでした。どこまでが一つのかたまりなのかが決まらないので、切り分けが完了しません。^ の印は、探し始めた場所を指しています。

どちらも SyntaxError ですが、文面が違います。 片方は組み立ての話をし、もう片方はかたまりの話をしています。 止まった場所が違えば、直す場所も違います。

n = 2
total = 3 + 4 * n

この二行は、最後まで機械語になりません。
機械語になっていたのは、これを読む側でした。

自分で書いて試してみる

ここに出てきたトークン・構文木・命令の列は、本物の CPython(3.12.11)が出したものです。 積み上げの様子だけは、命令の列から組み立てて描いています。

機械語は Linux 版の CPython から取り出しました。 このページ自体はブラウザの中で動いているので、そちらでは別の形に翻訳された CPython が動いています。

画面を短くするため、行の終わりや始まりを示す印は図から省いてあります。 省かずに全部見たいときは、プレイグラウンドで確かめられます。