皆さんの中には、Pythonを実行したとき__pycache__
というディレクトリができることを知っている方がいるかもしれません。
これはPythonスクリプトをバイトコードコンパイルしてできた.pycファイルをキャッシュしておくためのフォルダです。Pythonは直接的にはバイナリを読み取りながら実行しているのです。.pycファイルはVSCodeのhex editorで中を見てみるとこんな感じになっています。
Decoded textで解読可能なアルファベット以外は意味不明ですが、これから解説していくので恐れることはないです。この記事(シリーズ)を見た後のあなたはPythonバイトコードを完全に理解し、スクリプトのデバッギングに活用すらしているかもしれません。
動作確認済み環境
OS: Windows 10, Ubuntu 18.04
Python: CPython 3.9.0
逆アセンブラを使う
Pythonにはdis
という標準モジュールがあります。これはdisassemblerの略で、Pythonバイトコードを逆アセンブルして人間にも理解できる形にしてくれます。タイトルで人間でもわかるとか書いておいて早速ツール任せですが今は気にせず使っていきましょう。まずはシンプルにHello worldをコンパイルして逆アセンブルです。
print("Hello, world!")
逆アセンブル結果だけほしいので、ターミナルでpython -m dis <filename>.py
と打ってください。-mオプションは続く引数で指定したモジュールを__main__モジュールとして実行できます。そして結果がこちら。
1 0 LOAD_NAME 0 (print)
2 LOAD_CONST 0 ('Hello, world!')
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 1 (None)
10 RETURN_VALUE
↑行番号 ↑バイトオフセット、命令 ↑引数
次の章で一体何が起こっているのか解説しましょう。
inside Python
Pythonは実際の機械語のように命令(1バイト)+引数1つ(1バイト)を1ペアとして読み取り順番に実行します。これは固定長なので、1バイトの整数=0~255までなら引数として指定できますが、文字列などを直接指定するわけにはいきません。どうやってオブジェクトを取り扱っているのでしょうか。
先ほどのコードの解説に戻りましょう。まずはじめにLOAD_NAME 0
という命令があります。
Pythonインタープリタは一時計算結果をスタックで保持するスタックマシンです。一時的でないデータはテーブル(PythonでいうところのList/Tupleです。ていうか実際にListとTupleを使って実装されてます)を使って名前を保持します。その実体はPythonインタープリタがDictとして持っています。
引数に指定されている0は名前テーブル内でのインデックスです。Pythonはコードの実行単位としてコードオブジェクトなるものを使用しており、そのオブジェクト内には変数の名前や定数オブジェクトなどのテーブルが存在しています。これらのテーブルへインデックスごしにアクセスしているので、命令が固定長になるというわけです。
以下はCPythonでのコードオブジェクトの実装の一部分です。
struct PyCodeObject {
PyObject_HEAD
int co_argcount; /* #arguments, except *args */
int co_posonlyargcount; /* #positional only arguments */
int co_kwonlyargcount; /* #keyword only arguments */
int co_nlocals; /* #local variables */
int co_stacksize; /* #entries needed for evaluation stack */
int co_flags; /* CO_..., see below */
int co_firstlineno; /* first source line number */
PyObject *co_code; /* instruction opcodes */
PyObject *co_consts; /* list (constants used) */
PyObject *co_names; /* list of strings (names used) */
PyObject *co_varnames; /* tuple of strings (local variable names) */
PyObject *co_freevars; /* tuple of strings (free variable names) */
PyObject *co_cellvars; /* tuple of strings (cell variable names) */
...
};
co_names
に変数の名前が格納されていて、LOAD_NAME 0
ではその0番目の要素をとってきてスタックに載せるというわけです。disではご丁寧に0番目の要素が"print"という文字列であることまで表示してくれています。そしてco_consts
には定数(リテラル)が格納されていて、LOAD_CONST 0
でその0番目の要素をとってきてまたスタックに載せるというわけですね。
で、CALL_FUNCTION
でいよいよprint関数を呼びます。引数の1は関数がとる引数の数を指定しています。この命令はスタックの要素をn+1個消費して戻り値1個をスタックに返します。n個は引数、後の1個は関数の名前です。
次に終了処理です。スタックに残ったオブジェクトを取り除きます。
print関数はNoneを返すので、POP_TOP
でスタックトップからpopします(ちなみにこの命令は引数がいりませんが、引数は0としてあり合計2バイトであることに変わりはありません)。
それからPythonはスタックにNoneを載せてreturnし実行を終了するので、LOAD_CONST
でNoneを載せてRETURN_VALUE
しています。結果としてNoneを取り除いてまたNoneを載せるという無駄な手間をかけていますが、最後に実行される関数がprintみたくNoneを返すとは限らないのでこのようになっているようです。
他にもやってみる
次にこのようなコードを逆アセンブルしてみました。
from typing import Final
i: Final[int] = 1
FinalはPython3.8で追加された、変数(var)を定数(let)化する修飾子です。i = 2
などをこの後の行に追加すると~~エラーを吐きます。~~吐かないようです。意味ねえ。
結果は以下のようになりました。
1 0 SETUP_ANNOTATIONS
2 LOAD_CONST 0 (0)
4 LOAD_CONST 1 (('Final',))
6 IMPORT_NAME 0 (typing)
8 IMPORT_FROM 1 (Final)
10 STORE_NAME 1 (Final)
12 POP_TOP
3 14 LOAD_CONST 2 (1)
16 STORE_NAME 2 (i)
18 LOAD_NAME 1 (Final)
20 LOAD_NAME 3 (int)
22 BINARY_SUBSCR
24 LOAD_NAME 4 (__annotations__)
26 LOAD_CONST 3 ('i')
28 STORE_SUBSCR
1行目のモジュールのimportは面倒になるので次回以降解説します(予定)。
注目すべきは3行目です。STORE_NAME
は大体想像がつくと思いますがオブジェクトをco_namesの2番目の変数i
(実体はPythonインタープリタのDictにある)に格納します。LOAD_NAME
は逆に変数をロードするわけです。
そのあとのBINARY_SUBSCR
は少しトリッキーです。実はこの命令、スタックトップのオブジェクトをキーにしてスタックの二番目にあるオブジェクトにアクセスし、その結果をスタックトップに置く命令なのです。つまり、
TOS = TOS1[TOS]
ということです。TOSはTop Of Stackの略です。
なのでFinal[int]
という型指定は、実行上はFinalという名前のDict(-like object)にintという型オブジェクトをキーにしてアクセスすることと変わらないということになります。
そしてSTORE_SUBSCR
はTOS1[TOS] = TOS2
を実行します。つまり、__annotations__['i'] = Final[int]
ですね。__annotations__
というのは特殊なグローバル変数で、これによってPythonは後付けの静的型付け機能を実現しているようです。実行上は意味ねえ飾りですが。
おわりに
今回は以上です。ここまで読んでいただきありがとうございました。
次回は.pycファイルのフォーマットについて解説する予定です。