Skip to content

Latest commit

 

History

History
99 lines (69 loc) · 6.14 KB

File metadata and controls

99 lines (69 loc) · 6.14 KB

Pythonのインポートの仕組みと動的な検索パス解決 (sys.path)

Pythonで異なるディレクトリ(フォルダ)の間でスクリプトやパッケージをインポートしようとした際、多くの開発者が最初に遭遇する ModuleNotFoundError

LLM(大規模言語モデル)に限らず、あらゆるPythonプログラミングで必須となる**「Pythonがインポート対象を探し出す仕組み」と、書籍などに登場する「検索パスの動的な追加」**の仕組みについて、根本から詳しく解説します。


1. Pythonがインポート先を探す仕組み (sys.path)

Pythonのコード内で import A または from A import B と書いたとき、Pythonはファイルシステム全体を適当に探しているわけではありません。

Pythonは、**sys.path という「フォルダパスが登録された優先順位リスト」**を上から順番にチェックして、最初に見つかったフォルダからモジュール(ファイル)をロードします。

🔍 sys.path に入っている初期値

sys.path の中身をプリントすると、文字列でフォルダのパスが並んでいます。デフォルトでは、主に以下の場所が上から順に登録されています。

  1. 実行したスクリプトが置かれているフォルダ(カレントディレクトリ)
  2. PYTHONPATH というOSの環境変数に登録されているフォルダ(設定されている場合)
  3. Pythonの標準ライブラリ(mathos など)がインストールされているフォルダ
  4. 外部から pip インストールしたライブラリ(torchnumpy など)が保管されている site-packages フォルダ

2. なぜ ModuleNotFoundError が起きるのか?

以下のようなプロジェクト構造のとき、chap04/main.py から chap03/main.py を呼び出そうとします。

llm-sandbox/
├── chap03/
│   └── main.py  (インポートしたいモジュール)
└── chap04/
    └── main.py  (実行するメインスクリプト)

もし、そのまま chap04/main.py を実行した場合:

  1. Pythonは sys.path の一番最初(インデックス0)に、実行スクリプトがあるフォルダ(llm-sandbox/chap04/)を登録します。
  2. from chap03.main import ... という命令を実行した時、Pythonは sys.path の中に登録されたフォルダから chap03 という名前のフォルダを探します。
  3. しかし、llm-sandbox/chap04/ の中には、chap03 フォルダは存在しません。(一つ上の親フォルダの隣にあるため)。
  4. 結果、「そんなフォルダは見つかりません」となり、ModuleNotFoundError が発生します。

3. 解決策:検索パスの動的追加 (魔法のコードの解説)

書籍やサンプルコードでよく見かける以下の数行は、**「プログラムの実行時に、Pythonの探し物リスト (sys.path) にプロジェクトのルートフォルダのパスを最優先で割り込ませる」**という処理を行っています。

from pathlib import Path
import sys

# 1. 自分の親フォルダ、さらにその親フォルダ (ルート) の絶対パスを解決する
ROOT = Path(__file__).resolve().parents[1]

# 2. 検索パスリスト (sys.path) の最優先位置 (インデックス 0) に登録する
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

⚙️ 各要素のパーツ分解と役割

  • __file__: 現在実行されているスクリプトファイル自身(main.py)のパスを指す、Pythonの組み込み特殊変数です。
  • resolve(): シンボリックリンクや相対パス表記(../ など)を解決し、OSや実行環境に依存しない**完全な「絶対パス」**に変換します。
  • parents[N]: pathlib.Path の便利な機能で、パスを上の階層に遡ります。
    • parents[0]: 1つ上の親フォルダ(例: chap04/
    • parents[1]: 2つ上の親フォルダ(例: llm-sandbox/ ── これがプロジェクトのルートになります)
  • sys.path.insert(0, str(ROOT)): 検索先リスト sys.path は単なる Python のリスト(list)です。 append() を使うとリストの末尾(最悪優先)に追加されてしまいますが、insert(0, ...) を使うことで、**リストの先頭(最優先)**に割り込ませることができます。これにより、Pythonに真っ先にプロジェクトのルートフォルダを探しに行かせることができます。

4. 代替解決策:-m (モジュールモード) による実行

コードに sys.path.insert をいちいち書くのが面倒な場合、コマンドラインからPythonを実行するオプションを変えるだけで解決することもできます。

プロジェクトのルートフォルダ(llm-sandbox/)に移動した状態で、以下のように実行します。

# ルートに移動
cd C:\Users\owner\Documents\lab\llm-sandbox

# -m オプションを使い、ドット区切りの「モジュール」として実行する
python -m chap04.main

🚗 -m が裏でやっていること

-m(Moduleの略)を付けて実行すると、Pythonは**「あなたが現在コマンドを実行しているカレントディレクトリ(llm-sandbox/)」を自動的に sys.path の先頭(インデックス0番目)に設定**してくれます。

これにより、コード内に sys.path.insert を1行も書かなくても、自動的に from chap03.main import ... の解決が成功するようになります。


📂 関連ファイルリンク

  • インポート挙動実証デモ: import_demo.py (実際にインポートエラーが発生する様子、sys.path.insert によって解決する様子、および sys.path の優先順位リストが変化する様子を数値とコンソール上で完全に体感できるスクリプトです)