Q1Windows でも Linux でも壊れないパス組み立てとして推奨される書き方はどれですか?
os と pathlib — ファイルパスとディレクトリ操作
os.path.joinでWindows/Linuxのパス区切り差を吸収し、listdir/walk/globでディレクトリを再帰探索、pathlib.Pathの / 演算子と .stem / .suffixでパスを分解する書き方を実行で学びます。
ファイルパスとディレクトリを扱う 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の値はどれですか?