pythonファイルをコンパイルしてみた

"[プログラムはなぜ動くのか](https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AF%E3%81%AA%E3%81%9C%E5%8B%95%E3%81%8F%E3%81%AE%E3%81%8B-%E7%AC%AC%EF%BC%92%E7%89%88-%E7%9F%A5%E3%81%A3%E3%81%A6%E3%81%8A%E3%81%8D%E3%81%9F%E3%81%84%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%81%AE%E5%9F%BA%E7%A4%8E%E7%9F%A5%E8%AD%98-%E7%9F%A2%E6%B2%A2%E4%B9%85%E9%9B%84/dp/4822283151)"という本を読んで、プログラムがどのように実行されるのか勉強しています。

`C++`はコンパイラ言語なので、`g++ -o hello main.cpp`のようにコンパイルしてマシン語に変換します。

では、インタプリタ言語の一種である`Python`だと、どのようにマシン語に変換しているのか気になったので調べてみました。

- 実行環境
- 
```
$ python3 --version
Python3.7.3

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.4
BuildVersion:	18E226

$ ls
main.py utils/ keywords/
```


# ソースコードをコンパイルする

Pythonの公式ドキュメントを読むと、2通りの方法があるらしいことがわかりました。

コンパイルするファイルは、自作の[main.py](https://github.com/NaotoFujihiro/fetch-connpass-events-and-save/blob/master/main.py)を使いました。

## compileall を使う

https://docs.python.org/3/library/compileall.html

コンパイルしてみる。

```
$ python3 -m compileall main.py
```

出力結果は、`__pycache__/main.cpython-37.pyc`として保存されている。

バイトコード ・ファイルが出来上がるので、読みやすい形式にして出力する。

```
$ xxd -bits __pycache__/main.cpython-37.pyc
```

<img width="762" alt="compileall:main.cpython-37.pyc.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317198/2eb4f568-2b0f-d729-5bb7-66c340b6a79b.png">


## py_compile を使う

https://docs.python.org/3/library/py_compile.html

コンパイルしてみる。

```
$ python3 -m py_compile main.py
```

こちらも、出力結果は、`__pycache__/main.cpython-37.pyc`として保存されている。

バイトコード ・ファイルが出来上がるので、読みやすい形式にして出力する。

```
$ xxd -bits __pycache__/main.cpython-37.pyc
```

<img width="762" alt="py_compile:main.cpython-37.pyc.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/317198/385fd322-ec8e-d7fa-002e-40bcc64a455c.png">


## compileall と py_compile の差異

importしているモジュールを格納しているディレクトリ内のソースコードをコンパイルしてみる。

`compileall`だとコンパイルできる。

```
$ python3 -m compileall utils/
Listing 'utils/'...
Compiling 'utils/auth.py'...
Compiling 'utils/cache.py'...
Compiling 'utils/fetch.py'...
Compiling 'utils/folder.py'...
Compiling 'utils/generator.py'...
Compiling 'utils/sheet.py'...
```

一方、`py_compile`だとコンパイルできない。

```
$ python3 -m py_compile utils/
Traceback (most recent call last):
  File "/Users/Tachikoma/.pyenv/versions/3.7.3/lib/python3.7/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/Users/Tachikoma/.pyenv/versions/3.7.3/lib/python3.7/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/Users/Tachikoma/.pyenv/versions/3.7.3/lib/python3.7/py_compile.py", line 212, in <module>
    sys.exit(main())
  File "/Users/Tachikoma/.pyenv/versions/3.7.3/lib/python3.7/py_compile.py", line 204, in main
    compile(filename, doraise=True)
  File "/Users/Tachikoma/.pyenv/versions/3.7.3/lib/python3.7/py_compile.py", line 140, in compile
    source_bytes = loader.get_data(file)
  File "<frozen importlib._bootstrap_external>", line 916, in get_data
IsADirectoryError: [Errno 21] Is a directory: 'utils/'
```


# 中間生成ファイルはCFileとして出力される

Pythonの公式ドキュメントにも記述されていたように、コンパイル結果として、`[CFile](https://docs.microsoft.com/en-us/cpp/mfc/reference/cfile-class?view=vs-2019)`が生成されている。

`CFile`とは、MFCファイルの基底クラスらしい。


# デコンパイルしてみる

`.pyc` -> `.py` のデコンパイルをしてみる。

https://python-decompiler.com/

こちらのサイトを使って、デコンパイルすると、`compileall`, `py_compile`のどちらの場合でも、出力結果は同じとなった。


# 中間生成ファイルを実行してみた

`main.cpython-37.pyc`を実行できないかと思い、試してみた。

```
$ python3 __pycache__/main.cpython-37.pyc
Traceback (most recent call last):
  File "main.py", line 10, in <module>
    import keywords.InputSearchKeywordsFromConsole as Keywords
ModuleNotFoundError: No module named 'keywords'
```

importしているモジュールがないと怒られた。

`__pycache__/keywords/InputSearchKeywordsFromConsole.py`
`__pycache__/keywords/InputSearchKeywordsFromConsole.cpython-37.pyc`

どちらも試してみたが、うまくいかなかった。


# `__pycache__` とは

`python3 main.py`実行時に、importしているモジュールに対して、自動的に生成される。

```
$ ls
main.py utils/
$ ls utils/
fetch.py

$ python3 main.py

$ ls
main.py utils/
$ ls utils/
__pycache__/ fetch.py
```


# 追記

## dis を使って逆アセンブルしてみる

@shiracamus 様にコメント頂いたので、実際にコマンドを打ってみました。

ありがとうございます。


https://docs.python.org/ja/3/library/dis.html

逆アセンブルして、テキストファイルに出力してみる。

```
$ python3 -m dis main.py > __pycache__/dis.txt
```

[dis.txt](https://gist.github.com/NaotoFujihiro/45bfad24f54a253465eda5a36a2f2c17)

公式ドキュメントをみると、

```dis.txt
 26         100 LOAD_NAME               17 (print)
```

左から、`逆アセンブルする前の行番号`, `命令のアドレス`, `バイトコード命令`, `命令パラメタ`, `命令パラメタの解釈`を意味している。

バイトコード命令`LOAD_NAME`は、`プログラムはなぜ動くのか`の第１章で出てきたアセンブリ言語のニーモニックに似ている。

## python3 -m の -m について

https://docs.python.org/ja/3/using/cmdline.html

公式ドキュメントにあるように、

```
$ python3 -m <module> main.py
```

とすると、指定したmoduleをimportすることができます。


# 参考記事

https://qiita.com/sh-o/items/c0bc07a49c1939742569