ファイルパス操作には多くの落とし穴がある。ここでは実務でよく見かけるアンチパターンとその改善方法を紹介する。
文字列結合でパスを作る
最もよく見かけるアンチパターンは、+ や f-string でパスを結合することだ。
# ❌ アンチパターン
path = 'data/' + filename
path = f'{directory}/{filename}'
path = directory + '\\' + filename
この方法には複数の問題がある。
OS によってパス区切り文字が異なる(Windows は `\`、Unix 系は `/`)
末尾のスラッシュの有無で結果が変わる
パスの正規化が行われない
正しくは os.path.join() か pathlib を使う。
# ✅ 正しい方法
import os
from pathlib import Path
path = os.path.join(directory, filename)
path = Path(directory) / filename
ハードコードされたパス区切り文字
スラッシュやバックスラッシュを直接書くと、異なる OS で動かない。
# ❌ アンチパターン
config_path = 'config\\settings.ini' # Windows でしか動かない
log_path = '/var/log/app.log' # Unix 系でしか動かない
クロスプラットフォームで動かすには os.sep や pathlib を使う。
# ✅ 正しい方法
from pathlib import Path
config_path = Path('config') / 'settings.ini'
カレントディレクトリ依存のコード
相対パスを使うと、スクリプトの実行場所によって動作が変わる。
# ❌ アンチパターン
with open('config.json') as f: # どこから実行するかで結果が変わる
config = json.load(f)
スクリプトの場所を基準にするには __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)
存在確認と操作の間の競合
ファイルの存在を確認してから操作するまでの間に、状態が変わる可能性がある。
# ❌ アンチパターン(TOCTOU 競合)
if os.path.exists(filepath):
os.remove(filepath) # 確認後に別プロセスが削除していたらエラー
例外処理で対応するか、アトミックな操作を使う。
# ✅ 正しい方法
try:
os.remove(filepath)
except FileNotFoundError:
pass
# または Python 3.8 以降
from pathlib import Path
Path(filepath).unlink(missing_ok=True)
パスの比較を文字列で行う
パスの比較を文字列として行うと、同じファイルを指していても異なると判定されることがある。
# ❌ アンチパターン
path1 = '/home/user/../user/data.txt'
path2 = '/home/user/data.txt'
print(path1 == path2) # False(同じファイルなのに)
パスを正規化してから比較する。
# ✅ 正しい方法
from pathlib import Path
path1 = Path('/home/user/../user/data.txt').resolve()
path2 = Path('/home/user/data.txt').resolve()
print(path1 == path2) # True
ユーザー入力をそのまま使う
ユーザーからのパス入力をそのまま使うと、ディレクトリトラバーサル攻撃を受ける可能性がある。
# ❌ アンチパターン
filename = request.args.get('file')
path = os.path.join('/var/www/uploads', filename)
# filename が '../../../etc/passwd' だと危険
パスを正規化して、想定ディレクトリ内に収まっているか確認する。
# ✅ 正しい方法
from pathlib import Path
base_dir = Path('/var/www/uploads').resolve()
requested = (base_dir / filename).resolve()
if not str(requested).startswith(str(base_dir)):
raise ValueError('不正なパス')