pythonの-mオプションの動作を調べる

# -mオプションでの動作
通常のスクリプトでの実行との主な違いとしては、以下のように`sys.argv`と`sys.path`の先頭に追加されるパスが異なるという点のようです。

#### sys.argv
- スクリプト実行
    - `カレントディレクトリから実行されるファイルまでの相対パス`が追加される
- -mオプション付きで実行
    - `実行されるファイルの絶対パス`が追加される

#### sys.path
- スクリプト実行
    - `実行されるファイルが存在するディレクトリの絶対パス`が追加される。
- -mオプション付きで実行
    - `カレントディレクトリの絶対パス`が追加される。

# 公式ドキュメントでの説明
以降は細かい検証等になります。上記の説明よりも詳細が知りたいという方向けです。

まずは公式ドキュメントを参照します。
調べる際、最初は公式ドキュメントを見る癖付けをしなきゃなと最近感じています。

https://docs.python.org/ja/3/using/cmdline.html#cmdoption-m

公式ドキュメントの中で動作を説明しているのは以下の文章の部分になります。これを解読していきましょう。

> sys.path から指定されたモジュール名のモジュールを探し、その内容を __main__ モジュールとして実行します。

> このオプションが指定された場合、 sys.argv の最初の要素はモジュールファイルのフルパスになります (モジュールファイルを検索している間、最初の要素は "-m" に設定されます)。 -c オプションと同様に、カレントディレクトリが sys.path の先頭に追加されます。

## 前半部分
> sys.path から指定されたモジュール名のモジュールを探し、その内容を __main__ モジュールとして実行します。

### sys.path 
`sys.path`はモジュール検索パスと呼ばれ、外部モジュール（pandasやnumpyなど）をインポートする際のパス解決のリストとして使われます。
実際に`sys.path`を出力して見てみましょう。

```sh:コマンドラインで実行
$ python 
>>> import sys
>>> sys.path
['', '/Users/ryku/opt/anaconda3/lib/python38.zip', '/Users/ryku/opt/anaconda3/lib/python3.8', '/Users/ryku/opt/anaconda3/lib/python3.8/lib-dynload', '/Users/ryku/opt/anaconda3/lib/python3.8/site-packages', '/Users/ryku/opt/anaconda3/lib/python3.8/site-packages/aeosa']
```
これがモジュール検索パスです。
> sys.path から指定されたモジュール名のモジュールを探し

ということなので、例えば`python -m pandas`と実行した場合、まずは`''`の中（カレントディレクトリ）を見にいきます。その中に`pandas`が無かったら次は`/Users/ryku/opt/anaconda3/lib/python38.zip`ディレクトリの中にあるかどうかを見に行く...というような流れになります。

:::note info
最初の空文字''はカレントディレクトリを示しています。
> python -c codeおよびpython(REPL) コマンド ライン: 先頭に空の文字列を追加します。これは、現在の作業ディレクトリを意味します。

- 参考
    - https://docs.python.org/ja/3/library/sys.html#sys.path
:::

### mainモジュール
pythonのスクリプトは、`~.py`というファイルに保存された時点でモジュールになります。モジュールはpythonのインタラクティブモードからimportしたり、他のモジュールからimportしたりすることができます。
mainモジュールとは、実行のトップレベルや電卓モードでアクセスできる変数の集まり、と定義されています。

https://docs.python.org/ja/3/tutorial/modules.html#modules

:::note warn
電卓モードはインタラクティブモードと読み替えて良さそうです。
:::

#### モジュール実行例
例として以下のようなpythonファイルを作成します。

```python:sample_2.py
print(f"file: {__name__}")
```

:::note info
`__name__`という変数を使用して、そのファイルのモジュール名を取得できます。
:::

このモジュールを2種類の方法で呼び出します。

```sh:-mオプションで実行する
$ python -m sample_2.py 
file: __main__
```

```sh:モジュールをimportする
$ python
>>> import sample_2
file: sample_2
```


`python -m sample_2`として実行した際はmainモジュールとしてみなされ、`import`された際はmainモジュールにはなっていません。
> その内容を main モジュールとして実行します。

これで、↑がその通りだったということがわかりました。



## 後半部分
> このオプションが指定された場合、 sys.argv の最初の要素はモジュールファイルのフルパスになります (モジュールファイルを検索している間、最初の要素は "-m" に設定されます)。 -c オプションと同様に、カレントディレクトリが sys.path の先頭に追加されます。

### sys.argv/sys.pathに追加されるパス

以下のようにモジュール（パスは`/Users/ryku/Documents/python/research_py/lib/module.py`）を作成します。

```python:module.py
import sys

print(f"__name__ is: {__name__}")
print(f"argv is: {sys.argv}")
print(f"path is: {sys.path}")
```

これを2種類の方法で実行してみましょう。実行するディレクトリは`/Users/ryku/Documents/python/research_py`です。

```sh 
$ python lib/module.py
__name__ is: __main__
argv is: ['lib/module.py']
path is: ['/Users/ryku/Documents/python/research_py/lib', '/Users/ryku/opt/anaconda3/lib/python38.zip', '/Users/ryku/opt/anaconda3/lib/python3.8', '/Users/ryku/opt/anaconda3/lib/python3.8/lib-dynload', '/Users/ryku/opt/anaconda3/lib/python3.8/site-packages', '/Users/ryku/opt/anaconda3/lib/python3.8/site-packages/aeosa']
```

```sh
$ python -m lib.module 
__name__ is: __main__
argv is: ['/Users/ryku/Documents/python/research_py/lib/module.py']
path is: ['/Users/ryku/Documents/python/research_py', '/Users/ryku/opt/anaconda3/lib/python38.zip', '/Users/ryku/opt/anaconda3/lib/python3.8', '/Users/ryku/opt/anaconda3/lib/python3.8/lib-dynload', '/Users/ryku/opt/anaconda3/lib/python3.8/site-packages', '/Users/ryku/opt/anaconda3/lib/python3.8/site-packages/aeosa']
```

まずはargvを見てみましょう。`sys.argv`は実行時に渡されたコマンドライン引数のリストを返します。
公式ドキュメントの記載通り、スクリプトとして実行した場合、実行ディレクトリからモジュールまでの相対パスになっているのに対して、モジュールで実行した場合は絶対パスになっています。

次にpathを見てみます。こちらも公式ドキュメント通り、スクリプトの実行ではモジュールが存在しているディレクトリへの絶対パスがsys.pathの先頭に追加されているのに対して、-mオプションで実行した場合はカレントディレクトリ（実行した場所）の絶対パスがsys.pathの先頭に追加されています。

長々と検証を述べてきましたが、やはり-mオプションの重要な点としては、**スクリプトでの実行とで、この`sys.path`と`sys.argv`の先頭に追加されるパスが異なる**という点になるかと思います。なので冒頭の説明通りですね。

# 結局何の利点があるのか？
なんかパスに追加されるものが違うのはわかったけど、結局何が嬉しいの？となると思います。

![question_head_boy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/965436/7802f0e6-d047-66c5-9a01-877d77743dc4.png)


### 親ディレクトリのモジュールをインポートする必要のあるスクリプトの実行

パッと思いつくのはこれくらいですね..
自分の経験上では親ディレクトリのモジュールをインポートするスクリプトの実行時などにも`-mオプション`を使用していました。
親ディレクトリのインポートは`sys.path.append`してから`import package`する等の解決策もありますが、これはPEP8の規約に引っかかったりと色々面倒です。`-m`オプションを使うならスクリプトには実行ディレクトリからのパスでインポートするモジュールを指定すれば良いのでよりシンプルかなと思ってます。

:::note warn
`sys.path.append`の後にimport文を記述するとPEP8の`Module level import not at top of file (E402)`という警告が出てきます。
:::

# 付録：なんか__pycache__とかいうのができてるけど...

`-m`オプションでpythonファイルを実行すると、`__pychache__`という謎のファイルが生成されます。通常の実行では特に生成された記憶が無く、これはなんだろうと思っていました。
ChatGPTさんに聞いてみました。

:::note info 
--- Ryku（私） ---

pythonの__pycache__フォルダについて教えてください。

- __pycache__のメリット
- python -m sample.my_functionを実行した際に、__pycache__が生成されます。これは内部的にどう動いているのかまで具体的に説明
- 結び（何かPythonの学びになる一言を添えて）

という感じで解説をお願いします。
:::

:::note warn 
--- ChatGPT ---

理解しました。それでは、お願いされた内容に沿って説明いたします。

__pycache__とは、Pythonのバイトコードキャッシュ（事前にコンパイルされたPythonのソースコード）を格納するディレクトリです。このキャッシュは、再度実行する際にスクリプトを高速に実行するためのものです。

python -m sample.my_functionというコマンドを実行すると、「sample」というモジュールをインポートして「my_function」を実行するということになります。このとき、Pythonは「sample」モジュールに対する依存関係を調べ、それらを含むすべてのモジュールを探索します。このモジュールを探索するとき、「sample」モジュールのソースコードを再度コンパイルすることなく、__pycache__に保存されているバイトコードキャッシュを利用することができます。これにより、スクリプトの実行が高速になります。

結び：__pycache__はPythonのスクリプトの実行速度を向上させるためのもので、-mオプションを使って実行することでこのメリットを最大限に活用することができます。
:::

すごい的確に説明してそうなのでこれで良いですね。（もう記事を書く必要はあるのか...）