【Python】モジュール探索パスとファイル探索パスの違いについて

# 概要
**外部ライブラリや自作モジュールをインポート**する際に探索するパスと、**テキスト等のファイルを読込**する際に探索するパスが違うことに気づかずハマったことがあるので、備忘のために解説します。

# プロジェクトフォルダの構造（例）


```.フォルダ構造
project/
  ├ conf/
  │  └ conf.json
  └ src/
     ├ utils
     │  ├ fileutils.py
     │  └ remoteutils.py        ← fileutils.py をインポート、conf.jsonを読込
     └ func01/
        ├ syspath.py
        ├ filepath.py
        └ main.py               ← remoteutils.py をインポート
```

**コードの内容**
必要な説明は後述します。
```conf.json
{
    "conf" : {
        "content": "設定情報が読み込まれました。"
    }
}
```
```fileutils.py
import json
 
def get_json_data(path: str):
 
    # r:読み取り専用 -1:バッファリング無し, utf-8:文字コード
    with open(path, "r", -1, "utf-8") as file:
 
        file_data = file.read()
 
    json_data = json.loads(file_data)
 
    return json_data
```
```remoteutils.py
import src.utils.fileutils as fileutils

conf = fileutils.get_json_data("conf/conf.json")

def remoteutils_call():
    print(conf["conf"]["content"])
```
```main.py
import src.utils.remoteutils as remoteutils

def main():
    remoteutils.remoteutils_call()

if __name__ == "__main__":
    main()
```


## モジュールのインポート

### モジュール探索パス（PYTHONPATH）
モジュール探索パスとは、外部ライブラリや自作モジュールをインポートする際の探索の起点となるパスのことです。import文を記載する時に意識すべきで、sysライブラリを利用して探索パスを確認できます。

**モジュール探索パスの確認方法**
```syspath.py
import sys
import pprint

pprint.pprint(sys.path)
```

上記のスクリプトは**どこのディレクトリで実行しても同じ結果になる**ため、
ターミナルのカレントディレクトリがprojectでも、prpject\src\func01でも結果は変わりません。PYTHONPATHに対して手を加えていなければ次のような結果になるはずです。

**実行結果**

```.仮想環境をアクティブ化していない場合
[省略..\\project\\src\\func01\\,                ← syspath.pyを格納したパス
省略..\\Python\\Python312\\python312.zip',
省略..\\Python\\Python312\\DLLs',
省略..\\Python\\Python312\\Lib',
省略..\\Python\\Python312',
省略..\\Python\\Python312\\Lib\\site-packages']
```

```.仮想環境をアクティブ化している場合（仮想環境名："project"で作成）
[省略..\\project\\src\\func01\\,                ← syspath.pyを格納したパス
省略..\\Python\\Python312\\python312.zip',
省略..\\Python\\Python312\\DLLs',
省略..\\Python\\Python312\\Lib',
省略..\\Python\\Python312',
省略..\\Python\\Python312\\Lib\\site-packages'
省略..\\project',                               ← 仮想環境のルートパス
省略..\\project\\Lib\\site-packages']           ← 仮想環境の外部ライブラリインストール先
```

:::note warn
VSCodeのターミナルでsyspath.pyを実行すると、エディタの自動環境検出機能によって仮想環境をアクティブしていなくても仮想環境のモジュール探索パスが追加されていることがあります。
:::

:::note info
**モジュール探索パスは追加することが可能**
環境変数:PYTHONPATHを登録することで任意のパスを追加できます。
<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/626788/b8e70fd1-86f8-d118-ab1f-4e13158dc49a.png">

sysライブラリを用いて追加することもできます。
```addpythonpath.py
import sys

sys.path.append("省略../otherproject")
sys.path.append("省略../otherprojectLib/site-packages")
```
:::

### インポート文の実装

インポートではモジュール探索パスからの相対パスを指定します。


```.階層確認
project/
  ├ conf/
  │  └ conf.json
  └ src/
     ├ utils
     │  ├ fileutils.py
     │  └ remoteutils.py      ← fileutils.py をインポート
     └ func01/
        ├ syspath.py
        ├ filepath.py
        └ main.py             ← remoteutils.py をインポート
```

冒頭でも説明した通り、main.pyでは**remoteutils.pyをインポート**、
remoteutils.pyでは**fileutils.pyをインポート**します。（インポート文以外省略）

```main.py
import src.utils.remoteutils as remoteutils
```

```remoteutils.py
import src.utils.fileutils as fileutils
```
:::note info
**remoteutils.pyのインポート文について**
```remoteutils.py
import fileutils
```
remoteutils.pyとfileutils.pyが同じフォルダに配置されているため、上記でも可能です。（スクリプトが保存されたパスもモジュール探索パスに含まれる）
:::

## ファイルの読み込み
### ファイル探索パス

conf.jsonを読込する際はファイルパスを探索します。
ファイル探索パスは**スクリプトを実行した際のカレントディレクトリ**が基準です。
よって、実行した時のディレクトリによって結果が異なります。

**確認方法**
```filepath.py
import os

print(os.getcwd())
```

**実行結果**
```.projectフォルダからの相対パスで実行した場合
PS 省略..\project>Python src\func01\filepath.py
省略..\project
```

```.project\src\func01から実行した場合
PS 省略..\project\src\func01>Python filepath.py
省略..\project\src\func01
```

main.pyは**projectからの相対パス参照で実行する**ことを前提として、remoteutils.pyの実装をします。（パス指定部分以外省略）


```remoteutils.py
conf = fileutils.get_json_data("conf/conf.json")
```
:::note warn
**ファイルパスの指定方法について**
パスは"\\"区切りでも大丈夫ですが、その場合は文字列先頭に"r"をつけるか、"\\"をエスケープ（"\\\\"と記載）することを推奨します。
```remoteutils.py

# / 区切り指定
conf = fileutils.get_json_data("conf/conf.json")

# raw文字列指定
conf = fileutils.get_json_data(r"conf\conf.json")

# \ をエスケープ
conf = fileutils.get_json_data("conf\\conf.json")

# 非推奨（パスに"\n"や"\t"が含まれるとコケる）
conf = fileutils.get_json_data("conf\conf.json")

```

:::

**実行結果**
```.projectからの相対パス指定で実行した場合
PS ..省略\project>python src\func01\main.py
設定情報が読み込まれました。
```

:::note warn
**実行ディレクトリに注意**
```.project\src\fl01\jd01から実行した場合
PS ..省略\project\src\fl01\jd01> python .\main.py
Traceback (most recent call last):
  File "..省略\project\src\fl01\jd01\main.py", line 1, in <module>
    import src.utils.remoteutils as remoteutils
  File "..省略\project\src\utils\remoteutils.py", line 3, in <module>
    conf = fileutils.get_json_data("conf/conf.json")
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "..省略\project\src\utils\fileutils.py", line 6, in get_json_data
    with open(path, "r", -1, "utf-8") as file:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'conf/conf.json'
```

cdコマンド等で下記ディレクトリに移動してから実行するとエラーが発生します。
**PS ..省略\project\src\fl01\jd01> python .\main.py**

パス探索が下記のようになってしまうことが原因です。
**..省略\project\src\fl01\jd01\conf\conf.json**
:::

# まとめ

**モジュール探索パス**
* 外部ライブラリや、自作モジュールのインポートに利用される
* 起点となるパスは複数存在する
* 仮想環境のアクティブ化、非アクティブ化によってモジュール探索パスが変わる
* 任意にモジュール探索パスを追加することが可能
* モジュール探索パスからの相対参照のみ可能であり、絶対参照という概念は無い

**ファイル探索パス**
* テキストやExcelブックなどのファイルを参照する際に利用される
* 起点となるパスはスクリプト実行時のカレントディレクトリのみ
* 絶対パスの指定が可能