1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

os.pathをやめてpathlibにしたら、パス周りのバグが減った話

1
Posted at

何が問題だったか

Python でファイルを扱うとき、長らくこう書いていました。

import os

base = os.path.dirname(os.path.abspath(__file__))
data_dir = os.path.join(base, "data")
if not os.path.exists(data_dir):
    os.makedirs(data_dir)
path = os.path.join(data_dir, "result.csv")

動きます。動きますが、os.path.dirname(os.path.abspath(...))
毎回書くのがつらいし、os.path.join が入れ子になると読めません。

pathlib に変えるとこうなります。

from pathlib import Path

data_dir = Path(__file__).resolve().parent / "data"
data_dir.mkdir(parents=True, exist_ok=True)
path = data_dir / "result.csv"

パスが「文字列」ではなく「パスというオブジェクト」になるのが本質で、
そこから来る恩恵がいくつもあります。

/ で連結できる

いちばん目を引くのがこれです。

Path("data") / "2026" / "result.csv"
# WindowsPath('data/2026/result.csv')

割り算の演算子をパス連結に使うのは最初は奇妙ですが、
os.path.join(os.path.join("data", "2026"), "result.csv") と比べれば
読みやすさは明らかです。

区切り文字を自分で書かないので、OS の違いを気にしなくてよくなります。
Windows でも / で書けて、内部で \ に変換されます。

p = Path("data") / "result.csv"
print(p)         # Windows なら data\result.csv
print(p.as_posix())  # data/result.csv (常にスラッシュ)

文字列で "data/" + name と書いてしまうと、
末尾のスラッシュを忘れたり二重になったりする事故が起きます。

よく使うメソッド

場所を取る

p = Path("/home/user/docs/report.txt")

p.name        # 'report.txt'   ファイル名
p.stem        # 'report'       拡張子を除いた名前
p.suffix      # '.txt'         拡張子(ドット込み)
p.parent      # Path('/home/user/docs')
p.parents[1]  # Path('/home/user')

p.stem は地味にありがたいです。
os.path.splitext(os.path.basename(p))[0] を書かなくて済みます。

拡張子や名前を差し替える

p.with_suffix(".csv")   # Path('/home/user/docs/report.csv')
p.with_name("other.txt")  # Path('/home/user/docs/other.txt')

「同じ場所に拡張子だけ変えて保存」がこれ 1 行で書けます。

output = input_path.with_suffix(".json")

存在確認・作成

p.exists()      # ある?
p.is_file()     # ファイル?
p.is_dir()      # ディレクトリ?

d.mkdir(parents=True, exist_ok=True)

mkdir の 2 つの引数を覚えると、os.makedirs の分岐が消えます。

  • parents=True … 途中のディレクトリも作る(mkdir -p 相当)
  • exist_ok=True … 既にあってもエラーにしない

これが無いと FileExistsError を握りつぶす try を書く羽目になります。

読み書きのショートカット

小さいファイルなら open すら要りません。

text = p.read_text(encoding="utf-8")
p.write_text("こんにちは", encoding="utf-8")

data = p.read_bytes()
p.write_bytes(b"\x00\x01")

encoding は必ず明示します。 省略すると OS の既定に従うため、
Windows では cp932 になって、日本語で UnicodeDecodeError が出ることがあります。
(Python 3.15 で既定が UTF-8 になる予定ですが、明示する癖は付けておいて損はありません。)

大きいファイルは今まで通り open を使います。Path はそのまま渡せます。

with p.open(encoding="utf-8") as f:
    for line in f:
        ...

ファイルを探す

glob モジュールを別途 import しなくて済みます。

for csv in Path("data").glob("*.csv"):
    print(csv)

for py in Path("src").rglob("*.py"):   # 再帰的に
    print(py)

rglob("*.py")glob("**/*.py") と同じです。短いほうを使っています。

削除・リネーム

p.unlink()                 # ファイル削除
p.unlink(missing_ok=True)  # 無くてもエラーにしない
d.rmdir()                  # 空のディレクトリを削除
p.rename(p.with_suffix(".bak"))

ディレクトリを中身ごと消すのは pathlib にはありません。shutil.rmtree を使います。

__file__ からの相対パス

スクリプトと同じ場所にあるファイルを読みたい、というのはよくあります。

# ダメな例:カレントディレクトリ依存
config = Path("config.json")

これはどこから実行したかで結果が変わります。
python src/main.pycd src && python main.py で挙動が違う。

# 良い例:スクリプトの位置基準
BASE_DIR = Path(__file__).resolve().parent
config = BASE_DIR / "config.json"

resolve() はシンボリックリンクを解決して絶対パスにします。
これを付けておかないと parent. になることがあります。

パッケージ内なら、階層のぶんだけ .parent を重ねます。

# myapp/config.py から見て、myapp/ の 1 つ上(プロジェクトルート)
BASE_DIR = Path(__file__).resolve().parent.parent

標準ライブラリはだいたい Path を受け取る

Path に変えたら他のライブラリで動かないのでは」と心配になりますが、
Python 3.6 以降、標準ライブラリの多くは Path をそのまま受け取ります。

import json, sqlite3, csv

json.loads(path.read_text(encoding="utf-8"))
sqlite3.connect(db_path)          # Path でよい
open(path, encoding="utf-8")      # Path でよい

受け取ってくれない古いライブラリには str() で渡します。

some_old_api(str(path))

内部で文字列として保持したいときも、素直に変換してしまうのがトラブルが少ないです。

class Repository:
    def __init__(self, db_path):
        self.db_path = str(db_path)   # Path でも str でも受け取れる

os.path に残る利点

全部 pathlib にすべきかというと、そうでもありません。

  • 大量のパスをループで処理するとき、os.path のほうが速いことがあります
    Path オブジェクトの生成コストがある)
  • os.path.join は文字列を返すので、そのまま文字列連結したいときは素直

とはいえ、普通のアプリで問題になる差ではありません。
まず pathlib、困ったら os.path で十分だと思います。

まとめ

やりたいこと pathlib
連結 base / "sub" / "file.txt"
ファイル名 / 拡張子抜き / 拡張子 .name / .stem / .suffix
親ディレクトリ .parent
拡張子を変える .with_suffix(".csv")
ディレクトリ作成 .mkdir(parents=True, exist_ok=True)
読み書き .read_text(encoding="utf-8") / .write_text(...)
検索 .glob("*.csv") / .rglob("*.py")
スクリプト基準 Path(__file__).resolve().parent

パスを文字列として扱うのをやめただけで、
「区切り文字」「カレントディレクトリ依存」「拡張子の切り出し」あたりの
細かいバグがまとめて消えました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?