PythonでHello World

# Python で Hello World

[Python Advent Calendar 2023](https://qiita.com/advent-calendar/2023/python) の23日目の記事です。

## まえおき

Python入門記事ではないです。ご了承ください。

## Hello World

Pythonでは、以下のように書くことでHello Worldを出力することができます。

```python
print("Hello, World")
```

このコードを `main.py` という名前で保存し、ターミナルで以下のように実行します。

```bash
$ cat << EOF > main.py
print("Hello, World")
EOF

$ python main.py
Hello, World
```

Pythonのインタプリタを起動して、以下のように入力しても同じ結果が得られます。

```python
>>> print("Hello, World")
Hello, World
```

もっとも、この記事を読んでいる方であれば、既にご存知だと思います。

## Hello World の裏側で何が起こっているのか説明できますか？？

これは少し難しい質問です。「Pythonはインタプリタ型の言語なのでコードを逐次的に解釈して結果を表示している」のようなそれっぽい説明はできそうですが、あまり納得できる説明ではなさそうです。

ということで今回は、Pythonのインタプリタで何が起こっているのか、少し深掘ってみたいと思います。

:::note warn
おことわり

この記事内では、Python処理系としてCPythonを想定しています。断言はできませんが、例えばpypyのようなJITコンパイラを持つ処理系では、異なる挙動をとる可能性があります。また、Pythonのバージョンによっても挙動が異なる可能性があります。
:::

## 大まかなPythonの実行の流れ

Pythonに限った話ではないですが、インタプリタ型と呼ばれる言語の処理系は、大まかに以下のような流れで動作します。[^1]

[^1]: 厳密には最適化が入ることがあります。また、意味解析のスコープは実際と若干異なる気がします。。

1. プログラムのソースコードを読み込む
2. 字句解析: ソースコードをトークン列に変換する
3. 構文解析: トークン列を構文解析し、抽象構文木（ast）に変換する 
4. 意味解析: 抽象構文木をバイトコードに変換する
5. バイトコードを（仮想マシン上で）実行する

つまりこれらの処理を順番に追っていくことで、Pythonの実行の流れを大まかに理解することができるのではないでしょうか。

## 字句解析・構文解析の様子を観察する

Pythonでは、標準ライブラリの `ast` モジュールを使うことで、字句解析・構文解析の様子を観察することができます。

実際にHello Worldを解析してみましょう。

```python
import ast

source = """
print("Hello, World")
"""

tree = ast.parse(source)
print(ast.dump(tree))
```

実行すると以下のような結果が得られます。

```text
Module(body=[Expr(value=Call(func=Name(id='print', ctx=Load()), args=[Constant(value='Hello, World')], keywords=[]))], type_ignores=[])
```

少しわかりづらいですが、この結果を [ast --- 抽象構文木#抽象文法 (Abstract Grammar)](https://docs.python.org/ja/3/library/ast.html#abstract-grammar) と照らし合わせると、以下のように解釈できます。

- トップレベルのノードは `Module` で、これはPythonのモジュールまたはスクリプトであることを表している
- `Module` の中には `body` という属性があり、ここには実際のコードの実行内容が含まれている
- 今回は、`body` として `Expr` というノードが1つだけあり、これは式を表している
- `Expr` ノードの中には `Call` ノードがあり、これは関数呼び出しを表している
- この `Call` ノードは `print` 関数を呼び出している
- `args` は関数の引数を表し、今回は `Constant` ノードが1つだけあり、これは定数を表している
- この `Constant` ノードとして `"Hello, World"` という文字列を渡している
- keywords はキーワード引数を表しているが、今回は空

たったの一行のスクリプトですが、このように様々なノードに分解されていることがわかります。

## バイトコードがどのように生成されるのか観察する

Pythonでは、標準ライブラリの `dis` モジュールを使うことで、バイトコードの生成の様子を観察することができます。先ほど構文解析を行ったコードからバイトコードを生成してみます。

```Python
compiled_code = compile(tree, filename="<ast>", mode="exec")
bytecode = dis.dis(compiled_code)
```

これを実行すると以下のような結果が得られます。

```text
  0           0 RESUME                   0

  2           2 PUSH_NULL
              4 LOAD_NAME                0 (print)
              6 LOAD_CONST               0 ('Hello, World')
              8 PRECALL                  1
             12 CALL                     1
             22 POP_TOP
             24 LOAD_CONST               1 (None)
             26 RETURN_VALUE
```

[dis --- Python バイトコードの逆アセンブラ#Python バイトコード命令](https://docs.python.org/ja/3/library/dis.html#python-bytecode-instructions) によると、このバイトコードは以下のように解釈できます。

- `LOAD_NAME`: `print` という名前をスタックにプッシュする
- `LOAD_CONST`: `'Hello, World'` という定数をスタックにプッシュする
- `CALL`: スタックのトップから呼び出し可能な関数を、n 個の引数とともに取り出して呼び出す。ここでは `print` 関数を呼び出している
- `POP_TOP`: スタックのトップを取り出して破棄する。ここでは `print` 関数の戻り値を破棄している
- `LOAD_CONST`, `RETURN_VALUE`: `None` をスタックにプッシュして、関数から戻る

`Hello, World` という文字列を表示するために、`print` 関数を呼び出していることが確かに確認できました。

## バイトコードを実行する

先ほどのバイトコードの命令からも推測できるように、これらのバイトコードはスタックマシン上で実行されてます。
スタックマシンについての詳しい説明は割愛しますが、スタックと呼ばれるデータ構造に対してPushやPopといった操作を行うことで、プログラムを実行の実行を行うものです。

スタックマシンに関するより詳しい説明は [低レイヤを知りたい人のためのCコンパイラ作成入門#スタックマシン](https://www.sigbus.info/compilerbook#スタックマシン) の項などを参照してください。

さて、話を戻します。Python Developer’s Guideの [The bytecode interpreter (3.11)](https://devguide.python.org/internals/interpreter/) によると、バイトコードの実行を行うのは [ceval.c](https://github.com/python/cpython/blob/main/Python/ceval.c) というファイルだそうです。つまり、例えばgdbのようなデバッガを使ってこのファイル内で起こっていることを追っていくことで、バイトコードの実行の様子を観察することができます。

とはいえ、cpythonのソースコードは大変に複雑なので、実際にバイトコードの実行の様子を追うのはかなり大変です。少しずるいやり方ですが、バイトコードを解釈して実行する簡易的なシミュレータを実装して観察することにします。

簡単なスタックマシンの実装は以下のようになります。[^2] バイトコードの各命令はメソッドとして実装してあり、`simulate` メソッドを呼び出すことで、バイトコードの実行の様子を観察することができます。簡単のため、スタックマシンのインスタンスとして `stack` と `output` のみを持っており、`output` には各命令の実行後のスタックの状態が保存されていきます。

[^2]: このコードは構造的パターンマッチを用いているので、Python3.10以降のみで動作します。3.9以前のもので動作させたい場合は、if-elifなどを用いて適宜書き換えてください。

```Python
class PVM:
    def __init__(self):
        self.stack = []
        self.output = []

    def LOAD_NAME(self, name):
        self.stack.append(name)

    def LOAD_CONST(self, const):
        self.stack.append(const)

    def CALL(self, arg_count):
        args = [self.stack.pop() for _ in range(arg_count)]
        func = self.stack.pop()
        result = func(*args)
        if result is not None:
            self.stack.append(result)

    def POP_TOP(self):
        if self.stack:
            self.stack.pop()

    def simulate(self, instructions):
        for instr, arg in instructions:
            match instr:
                case "LOAD_NAME":
                    self.LOAD_NAME(arg)
                case "LOAD_CONST":
                    self.LOAD_CONST(arg)
                case "CALL":
                    self.CALL(arg)
                case "POP_TOP":
                    self.POP_TOP()

            self.output.append(list(self.stack))

        return self.output
```

さて、このスタックマシンを使って、先ほどのバイトコードを実行してみましょう。

```Python
instructions = [
    ("LOAD_NAME", print),
    ("LOAD_CONST", "Hello, World"),
    ("CALL", 1),
    ("POP_TOP", None),
]

pvm = PVM()
stack_states = pvm.simulate(instructions)
```

実行すると、以下のような結果が得られます。

```text
Hello, World
```

無事に `Hello, World` という文字列が表示されました。めでたいですね。また、各命令を実行した後のスタックの状態は、以下のようにして確認できます。

```python
for i, state in enumerate(stack_states):
    print(f"{i}: {state}")
```

これを実行すると以下のような結果が得られます。[^3]

[^3]: pythonのprint文は任意のオブジェクトに対して実行でき、pythonでは全てがオブジェクトとして表現されているので、組み込み関数でさえもprintできます。便利ですね。

```text
0: [<built-in function print>]
1: [<built-in function print>, 'Hello, World']
2: []
3: []
```

確かにスタックの状態が、バイトコードの実行の様子を反映していることが確認できます。

## まとめ

ということで、Pythonの処理系がどのように動いているのか少し深掘ってみました。
Pythonの処理系は、ソースコードを字句解析・構文解析して抽象構文木に変換し、意味解析してバイトコードに変換し、バイトコードを実行することで、プログラムを実行していることが確認できました。
今回は文字を表示するだけの簡単なプログラムでしたが、より複雑なプログラムを実行する場合でも、同じような手続きで実行されていることを確認できると思います。

## 参考資料

- [言語処理系とは](https://www.hpcs.cs.tsukuba.ac.jp/~msato/lecture-note/comp-lecture/note1.html)
- [スタックマシン](http://www.hpcs.cs.tsukuba.ac.jp/~msato/lecture-note/comp-lecture/note8.html)
- [プログラミング言語処理系の基礎](http://kmizu.github.io/hands-on/#/)
- [ast --- 抽象構文木#抽象文法 (Abstract Grammar)](https://docs.python.org/ja/3/library/ast.html#abstract-grammar)
- [dis --- Python バイトコードの逆アセンブラ#Python バイトコード命令](https://docs.python.org/ja/3/library/dis.html#python-bytecode-instructions)
- [Pythonの処理系はどのように実装され，どのように動いているのか？我々はその実態を調査すべくアマゾンへと飛んだ．](https://sugarheart.utgw.net/static/pdf/20170308-inside-python-interpreter.pdf)
- [Read Inside The Python Virtual Machine](https://leanpub.com/insidethepythonvirtualmachine/read)