【Python】実行する場所でファイルが見つからない:相対パスの基準をそろえる

PythonのTopに戻る

相対パスは「どこから起動したか」で変わる

同じスクリプトなのに、端末では動いてIDEや別のフォルダーから起動すると入力ファイルが見つからない。その場合は、ファイル名より先に相対パスの基準を確認する。Path("data/input.txt")は通常、スクリプトの置き場所ではなく、現在の作業ディレクトリを基準にする。スクリプトに隣接するデータを読みたいなら、__file__から基準を組み立てる。

スクリプト同梱の入力、利用者が指定した入力、生成する出力は、必ずしも同じ基準にする必要がない。例えばテンプレートはスクリプト相対、利用者の入力はコマンド引数、出力は明示した作業フォルダーと分けられる。基準を決めずにすべて絶対パスへ書き換えると、別のPCで動かしにくくなるので、パスの役割を先に整理する。

起動場所を変えても同じ同梱データを読む

次の二つのファイルを同じフォルダーへ保存し、example.pyを実行する。後半の例が一時ディレクトリ内へ小さなプロジェクトを作り、別の作業ディレクトリからapp.pyを起動する。手元の既存データは変更しない。

app.py

from pathlib import Path

BASE = Path(__file__).resolve().parent
INPUT = BASE / "data" / "input.txt"
print(INPUT.read_text(encoding="utf-8").strip())

example.py

from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess
import sys

with TemporaryDirectory() as temporary:
    root = Path(temporary)
    project = root / "project"
    (project / "data").mkdir(parents=True)
    (project / "data" / "input.txt").write_text("sample A\n", encoding="utf-8")
    app = project / "app.py"
    source = Path(__file__).with_name("app.py").read_text(encoding="utf-8")
    app.write_text(source, encoding="utf-8")
    other = root / "other"
    other.mkdir()
    assert not (other / "data" / "input.txt").exists()
    result = subprocess.run(
        [sys.executable, str(app)], cwd=other,
        text=True, capture_output=True, check=True,
    )
    assert result.stdout == "sample A\n"
    print("started outside project:", result.stdout.strip())

実行結果

started outside project: sample A

cwdと__file__は別の情報

Path.cwd()はプロセスの現在の作業ディレクトリを返す。__file__は読み込まれたスクリプトやモジュールのファイル位置を示す。resolve()で絶対的なパスへそろえ、parentでその親フォルダーを取得してからdata/input.txtを付けている。/演算子は文字列の割り算ではなく、Path同士をつなぐ操作である。

例ではsubprocess.run()のcwdへotherを指定した。相対パスのdata/input.txtはそこには存在しないが、app.pyは自分の場所から入力を探すため成功する。単に手元で一度読めたことだけでなく、起動場所を意図的に変えて確認することで、この修正の目的を検証している。sys.executableは今のPythonと同じ実行ファイルで子プロセスを起動するために使う。

os.chdir()でスクリプトのフォルダーへ移動する方法もあるが、作業ディレクトリはプロセス全体に影響する。後から別の関数が相対パスを使うと、その解釈まで変わってしまう。必要なパスを基準から組み立てて関数へ渡せば、呼び出し元の状態を変えずに済む。

Notebook・配布パッケージ・シンボリックリンク

対話シェルやNotebookには、通常の.pyファイルと同じ意味の__file__がない。そこで無理にこの式を使うのではなく、プロジェクトの基準フォルダーを変数や設定で明示する。Notebookを保存した場所と作業ディレクトリが必ず一致する、とも決めつけない。まずPath.cwd()で実際の基準を確認する。

resolve()はシンボリックリンクも解決するため、リンクを置いた場所ではなく実体のある場所が基準になる。リンクの隣に設定を置く設計なら、目的と合わない可能性がある。リンク経由の起動を認めるか、実体に同梱するかを決めてからパスの作り方を選ぶ。

インストール可能なパッケージに含めたデータでは、実ファイルの隣にあることを仮定しない方がよい場合がある。その用途にはimportlib.resourcesが用意されている。ここでの方法は、通常の.pyファイルと、その隣に置いたファイルを一緒に管理する小さなスクリプト向けである。

出力先まで自動的に同じ場所へしない

読み込みに成功しても、スクリプトの場所に書き込み権限があるとは限らない。配布済みツールの隣へ結果を保存するより、利用者が指定した出力先を使う方が扱いやすい。入力と出力の絶対パスを開始時に確認できるようにすると、別のデータを読んだまま解析が進むミスも減らせる。実行記録へ残す場合は個人名などを含むパスの共有範囲にも注意する。

実行環境と関連情報

掲載コードはLinux上のCPython 3.12.14で実行した。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。

関連:インストール後も同梱データを読めるようにする:importlib.resources / 保存先が指定フォルダーの外に出ないようにする:パスの検証

PythonのTopに戻る