相対パスを使ったコードは、カレントディレクトリ(作業ディレクトリ)が変わると壊れる。これはよくあるバグの原因だ。
問題の例
# ❌ カレントディレクトリに依存するコード
# script.py
with open('config.json') as f:
config = json.load(f)
このスクリプトは、スクリプトと同じディレクトリから実行すれば動く。
cd /home/user/myapp python script.py # 動く
しかし、別のディレクトリから実行すると動かない。
cd /home/user python myapp/script.py # FileNotFoundError
config.json は /home/user/config.json を探しに行くが、実際には /home/user/myapp/config.json にあるからだ。
なぜカレントディレクトリが変わるのか
カレントディレクトリが変わる状況は多い。
ユーザーが異なるディレクトリからスクリプトを実行する
cron やサービスとして実行される(カレントディレクトリが / や /root になる)
IDE やテストフレームワークがプロジェクトルートから実行する
コード内で os.chdir() を呼んでいる
__file__ を使った解決方法
スクリプトの場所を基準にパスを解決すれば、どこから実行しても動く。
# ✅ スクリプトの場所を基準にする
from pathlib import Path
script_dir = Path(__file__).resolve().parent
config_path = script_dir / 'config.json'
with open(config_path) as f:
config = json.load(f)
__file__ はスクリプト自身のパスを持つ特殊変数だ。resolve() で絶対パスに変換し、parent でディレクトリ部分を取得する。
os.path を使う場合
pathlib を使わない場合は os.path で同じことができる。
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
config_path = os.path.join(script_dir, 'config.json')
パッケージ内のリソースファイル
パッケージ内のリソースファイルにアクセスする場合は importlib.resources を使う方が確実だ。
# Python 3.9 以降
from importlib.resources import files
config_text = files('mypackage').joinpath('config.json').read_text()
__file__ はパッケージが zip 化されている場合などに使えないことがあるが、importlib.resources はそのような状況でも動作する。
os.chdir() の危険性
コード内で os.chdir() を使うと、以降のすべての相対パスに影響する。
# ❌ 危険なコード
os.chdir('/tmp')
# この後のすべての相対パスが /tmp 基準になる
with open('data.txt') as f: # /tmp/data.txt を開こうとする
pass
どうしても chdir() が必要な場合は、コンテキストマネージャで元に戻す。
import os
from contextlib import contextmanager
@contextmanager
def working_directory(path):
old = os.getcwd()
try:
os.chdir(path)
yield
finally:
os.chdir(old)
# 使用例
with working_directory('/tmp'):
# ここだけカレントディレクトリが /tmp
pass
# 元のディレクトリに戻る
ベストプラクティス
相対パスは極力使わず、`__file__` 基準の絶対パスを使う
`os.chdir()` は避けるか、必ず元に戻す
設定ファイルのパスは環境変数や引数で渡せるようにする