Q1Windows でも Linux でも壊れないパス組み立て として推奨される書き方はどれですか?
os と pathlib — ファイルパスとディレクトリ操作
Python の os.path と pathlib モジュールを基礎から解説します。OS の差を吸収するパス組み立て、ディレクトリの一覧と再帰探索、Path オブジェクトでのパス分解まで、ハンズオンで学べます。
ファイルパスとディレクトリを扱う 2 つのモジュール — 古くから使われる os.path と、より新しく書きやすい pathlib を整理します。OS の差を吸収するパス組み立て、ディレクトリの一覧と再帰探索、Path オブジェクトでのパス分解までを順に見ていきます。
os.path — OS の差を吸収するパス組み立て
ファイルパスの区切り文字は OS によって違います。Windows は \(バックスラッシュ)、Linux と macOS は / です。コードに直接 "data/sales/2024.csv" と書く と Linux と Mac では動きますが、Windows 上で実行したときに パスを誤認 することがあります。
os.path.join("data", "sales", "2024.csv") のように 要素ごとに区切って渡す と、Python が動いている OS に合わせて適切な区切り文字をその場で選んでくれます。
/ 区切り、Windows では \ 区切りに展開される。手書きで区切り文字を埋め込まない ことが、移植性を保つコツ。| 関数 | 意味 | 例 |
|---|---|---|
| os.path.join(*parts) | OS の区切りでパスを連結する | join('data', 'sales') → 'data/sales' |
| os.path.exists(p) | パスが存在するか | True / False |
| os.path.isfile(p) | ファイルか(ディレクトリではない) | True / False |
| os.path.isdir(p) | ディレクトリか | True / False |
| os.path.basename(p) | 末尾のファイル名・フォルダ名 | basename('data/x.csv') → 'x.csv' |
| os.path.dirname(p) | 末尾を取り除いた親パス | dirname('data/x.csv') → 'data' |
| os.path.splitext(p) | 拡張子を分離する | splitext('x.csv') → ('x', '.csv') |
実践 2 — basename と splitext で名前と拡張子に分ける
1 行に詰め込まず、段階的に変数へ受け取りながら ファイル名と拡張子を分離します。basename でフルパスから末尾のファイル名を取り出し、splitext でその名前を (名前, 拡張子) のタプルに分けます。
os.listdir と os.walk — ディレクトリの一覧と再帰探索
フォルダの中身を Python から取り出すとき、1 階層だけ見る なら os.listdir、サブフォルダまで再帰的に見る なら os.walk を使います。os.listdir は指定したフォルダの直下にある名前(ファイルとサブフォルダ両方)をリストで返し、os.walk は配下のすべての階層を再帰的に辿って (現在のパス, サブフォルダ名のリスト, ファイル名のリスト) のタプルを 1 階層ずつ生成します。
import os
# 1 階層: 'data' 直下の名前
print(os.listdir("data"))
# → ['sales', 'inventory']
# 再帰: 'data' 配下を全部辿る
for dirpath, dirnames, filenames in os.walk("data"):
print(dirpath, filenames)
# → data ['sales', 'inventory'] []
# data/sales [] ['2024_q1.csv', '2024_q2.csv']
# data/inventory [] ['items.json']
glob — パターンマッチでファイルを集める
拡張子が `.csv` のものだけ のように、条件に合うファイルだけ集めたいときは glob モジュールが最短です。*(任意の文字列)や **(任意の階層)といったワイルドカードで対象パターンを書くと、それに合うパスのリストが返ります。
* は同一階層内の任意の文字列、** は任意の階層をまたぐ(recursive=True 必須)。import glob
# data/sales 直下の CSV ファイル
print(glob.glob("data/sales/*.csv"))
# → ['data/sales/2024_q1.csv', 'data/sales/2024_q2.csv']
# data 配下を再帰的に検索(** + recursive=True)
print(glob.glob("data/**/*.csv", recursive=True))
# → ['data/sales/2024_q1.csv', 'data/sales/2024_q2.csv']
glob のワイルドカード ** は recursive=True とセット
glob.glob("data/**/*.csv") のパターンにある 二重アスタリスク は、何階層でも辿るワイルドカードです。ただし recursive=True を渡さないと普通の * と同じ扱いになり、深いフォルダが見つかりません。再帰検索したいときは必ずこの引数を付けます。
pathlib.Path — オブジェクト指向のパス操作
os.path は 文字列でパスを扱う ライブラリでしたが、Python 3.4 以降は パス自体を 1 つのオブジェクトとして扱う pathlib.Path が推奨されています。Path("data/sales/2024_q1.csv") のように作ると、.parent で親フォルダ、.name で末尾、.stem で拡張子なしの名前、.suffix で拡張子 といった属性で各部分にアクセスできます。
os.path.dirname / basename / splitext を別々に呼ぶより読みやすい。from pathlib import Path
p = Path("data") / "sales" / "2024_q1.csv" # / 演算子で連結
print(p) # data/sales/2024_q1.csv
print(p.parent) # data/sales
print(p.name) # 2024_q1.csv
print(p.stem) # 2024_q1
print(p.suffix) # .csv
print(p.exists()) # True
# 中身を読む(with open のラッパー)
print(p.read_text()) # CSV の中身
# サブフォルダの一覧(os.walk 相当)
for sub in Path("data").rglob("*.csv"):
print(sub)
`os.path` は文字列ベース、`pathlib.Path` はオブジェクトベース で同じ操作を提供します。下の表でやりたいことごとに対応関係を整理します。
| やりたいこと | os.path 系 | pathlib 系 |
|---|---|---|
| 連結 | os.path.join('data', 'x.csv') | Path('data') / 'x.csv' |
| 親フォルダ | os.path.dirname(p) | p.parent |
| ファイル名 | os.path.basename(p) | p.name |
| 拡張子なしのファイル名 | os.path.splitext(p)[0] の処理 | p.stem |
| 拡張子 | os.path.splitext(p)[1] | p.suffix |
| 存在確認 | os.path.exists(p) | p.exists() |
| 再帰検索 | glob.glob('**/*.csv', recursive=True) | Path('.').rglob('*.csv') |
| 読み込み | with open(p) as f: f.read() | p.read_text() |
新しいコードは pathlib を選ぶ
新規コードは pathlib を使うのが推奨 です。古いライブラリの API が文字列パスを要求する場面(例: 一部の DB ドライバ)では str(p) で変換すれば渡せます。os.path が消えるわけではないので、既存コードを読むためにも両方の対応関係を知っておくと安心です。
理解度チェック
まずは1問ずつ答えてみましょう。
Q2次のうち、フォルダの中身を再帰的に全階層辿る のに最も向いているのはどれですか?
Q3p = Path("data/sales/2024_q1.csv") のとき、p.stem の値はどれですか?