はじめに
pathlib
はパスを表すクラスを提供してくれます。このクラスには様々なメソッドが備わっており、os.pathのように文字でパスを扱うよりも簡単で便利に処理を記述することができます。
使い方を知るためには公式ドキュメントを読むのが良いのですが、(私が)使わないメソッドも多く記載されているので本記事ではよく使うメソッドに絞って紹介します。
環境
ディレクトリ構造
紹介の前に環境のお話です。今回は以下のような構造を対象として処理結果を例示します。
data
には年月日をyymmdd
形式で表現したデータが格納されているとイメージしてください。また、一部datetime
を使ったほうが良い表現もありますが、今回はpathlib
の説明のため使いません。
C:\pathlib_demo>tree /f
C:.
├─data
│ ├─230101
│ ├─230201
│ │ demo1.csv
│ │ demo1.tsv
│ │
│ ├─230202
│ │ demo1.csv
│ │ demo2.csv
│ │
│ ├─230203
│ └─230211
└─pathlib_demo
test.py
ちなみにこのディレクトリ構造は以下のコードを実行することで生成できます。
せっかくなのでpathlibで行っています。各処理で使用しているメソッドの解説は下の方で行います。
from pathlib import Path
# パスオブジェクトを生成
project_path = Path("C:\pathlib_demo")
data_path = project_path / "data"
program_path = project_path / "pathlib_demo"
# フォルダ生成
for i in ["101", "201", "202", "203", "210", "211"]:
(data_path / f"230{i}").mkdir(parents=True)
program_path.mkdir(parents=True)
# ファイル生成
(data_path / "230201" / "demo1.csv").touch()
(data_path / "230201" / "demo1.tsv").touch()
(data_path / "230202" / "demo1.csv").touch()
(data_path / "230202" / "demo2.csv").touch()
(program_path / "test.py").touch()
以降のコードについてはtest.py
に記述すると同じ結果が得られます。
インストール
python3.4以降では標準ライブラリなのでインストールは不要です。
以下のようにメインクラスをインポートします。
from pathlib import Path
バージョンによって挙動が異なるので環境には注意しましょう。
コードの紹介
実行中のプログラムのファイルパスを取得する
__file__
を使うことでどこから呼び出しても自分の位置を知ることができます。
current_file_path = Path(__file__)
print(current_file_path)
> c:\pathlib_demo\pathlib_demo\test.py
実行中のプログラムのフォルダパスを取得する
os.pathのような文字列で絶対パスを指定すると作業環境を移動させたときに不具合が出ますが、これなら安心です。
current_folder_path = Path(__file__).parent
print(current_folder_path)
> c:/pathlib_demo/pathlib_demo
jupyterの場合は__file__
が使えませんが別の手段で同じことができます。
import os
current_folder_path = Path(os.path.abspath(" ")).parent
print(current_folder_path)
> c:/pathlib_demo/pathlib_demo
上の階層にアクセスする
フォルダパスを取得したときに使用したparent
はパスを一つ上の階層に移動することができます。
project_path = current_folder_path.parent
print(project_path)
> c:\pathlib_demo
parent
は繰り返し使用することで、さらに上の階層に移動することができます。
しかし、可読性が下がるのでおすすめはしません。
print(current_folder_path.parent)
print(current_folder_path.parent.parent)
> c:\pathlib_demo
> c:\
parent
を繰り返し使いたい場合はparents
を使うとすっきりと書けます。
インデックスに指定した値+1個上の階層に移動します。
print(current_folder_path)
print(current_folder_path.parents[0])
print(current_folder_path.parents[1])
> c:\pathlib_demo\pathlib_demo
> c:\pathlib_demo
> c:\
パスを指定する
/
と文字列で指定することが出来ます。直感的で分かりやすいですね。
data_path = project_path / "data"
print(data_path)
> c:\pathlib_demo\data
osの違いを吸収してくれるのも嬉しいポイントです。
文字列でパスを扱う場合は区切りとしてwindowsでは\
、Linuxでは/
を使うため厄介ですがpathlibを使うことでユーザーはその違いを考える必要がなくなります。
これによりコードも共通化することができます。
current_path = Path(__file__).parent
data_path = current_path / "data"
print(data_path)
print(type(data_path))
> c:\pathlib_demo\data
> <class 'pathlib.WindowsPath'>
current_path = Path(__file__).parent
data_path = current_path / "data"
print(data_path)
print(type(data_path))
> /root/data
> <class 'pathlib.PosixPath'> # Linuxの場合は自動的に非windowsパスを表すサブクラスになります
ディレクトリ配下のパスを取得する
pathlibオブジェクトではglob
を使用することができます。
"*"
で指定するとディレクトリ配下の全てのパスを返してくれます。
for i in data_path.glob("*"):
print(i)
> c:\pathlib_demo\data\230201
> c:\pathlib_demo\data\230202
> c:\pathlib_demo\data\230203
> c:\pathlib_demo\data\230211
glob
の戻り値はgenerator
です。list
として扱いたい場合はlist()
をつけて下さい。
from pprint import pprint # listの出力は横に広がって見にくいためpprintを使用
l = list(data_path.glob("*"))
pprint(l) # WindowsPathはWindows用のパスであることを示します。使用の際は深く考えなくて良いです。
> [WindowsPath('c:/pathlib_demo/data/230201'),
WindowsPath('c:/pathlib_demo/data/230202'),
WindowsPath('c:/pathlib_demo/data/230203'),
WindowsPath('c:/pathlib_demo/data/230211')]
glob("*")
を使わなくてもdata_path.iterdir()
でも同じ結果を得ることができます。
処理目的を明示する場合は後者を使った方が良いかもしれませんが、覚えることを減らす目的から他の用途にも使えるglob
で紹介をしました。
パターン検索でパスを取得する
glob
は与える引数によって取得するパスを指定することができます。
といっても覚えるのは?
, *
, []
の三種です。
曖昧な検索ができる"?"と"*"
?
は 任意の文字(1文字分) の代わりになります。一文字分曖昧に検索ができると考えると良いです。 ちなみにさきほどから使用している*
は 任意の文字列(適当な文字数) の代わりになります。そういった理由でglob("*")
は全てのパスを取得するパターンとなります。
pprint(list(data_path.glob("23020?"))) # 2月の上旬
> [WindowsPath('c:/pathlib_demo/data/230201'),
WindowsPath('c:/pathlib_demo/data/230202'),
WindowsPath('c:/pathlib_demo/data/230203')]
pprint(list(data_path.glob("2302?1"))) # 2月の1のつく日
> [WindowsPath('c:/pathlib_demo/data/230201'),
WindowsPath('c:/pathlib_demo/data/230211')]
pprint(list(data_path.glob("2302*"))) # 2月
> [WindowsPath('c:/pathlib_demo/data/230201'),
WindowsPath('c:/pathlib_demo/data/230202'),
WindowsPath('c:/pathlib_demo/data/230203'),
WindowsPath('c:/pathlib_demo/data/230211')]
厳密な検索ができる"[]"
?
は任意の一文字でしたが、[]
は指定した一文字を表します。
[12]
とすれば1か2が対象になります。連番の場合は[1-2]
とすると可読性が上がります。
アルファベットの場合も同様に連番を指定することができます。例えば[a-c]
と指定するとa,b,cが対象になります。
[!]
を使うとその文字以外が指定されます。
pprint(list(data_path.glob("23020[1-2]"))) # 1日と2日
> [WindowsPath('c:/pathlib_demo/data/230201'),
WindowsPath('c:/pathlib_demo/data/230202')]
pprint(list(data_path.glob("23020[!1-2]"))) # 2月上旬の1日と2日以外
> [WindowsPath('c:/pathlib_demo/data/230203')]
# *と組み合わせることもできます
pprint(list(data_path.glob("23*[1-2]"))) # 2023年の任意の月の1日と2日
> [WindowsPath('c:/pathlib_demo/data/230203')]
"[]"を使った指定方法はre
モジュールによる正規表現に似ていますが挙動が異なります。
globのパターン指定についてはfnmatchのドキュメントを参照してください。
パターン検索についてまとめたものがこちらです。
パターン | 文字数 | 対象 |
---|---|---|
? | 一文字 | 任意 |
* | 複数文字 | 任意 |
[文字] | 一文字 | 指定した文字 |
[!文字] | 一文字 | 指定した文字以外 |
再帰的に取得する
ディレクトリを指定した上でさらに下層のパスを取得することができます。
print(list(data_path.glob("230201/*"))) # 2月1日の下層
> [WindowsPath('c:/pathlib_demo/data/230201/demo1.csv'),
WindowsPath('c:/pathlib_demo/data/230201/demo1.tsv')]
Linuxについても同様の指定方法で取得できます。
ファイル名がある層まで到達しました。
指定した拡張子のみを検索する場合は、さきほど紹介した*
を使ってファイル名の部分を曖昧に検索してあげれば良いです。
pprint(list(data_path.glob("230201/*.tsv")))
> [WindowsPath('c:/pathlib_demo/data/230201/demo1.tsv')]
ディレクトリ生成
mkdir
メソッドを使用することでディレクトリを生成できます。
tests_path = project_path / "tests"
tests_path.mkdir()
# c:\pathlib_demo\testsが生成されます
(project_path / "tests").mkdir() # こちらも同じ挙動をします。
mkdir
メソッドは引数を指定することで便利に使うことができます。
exits_ok
mkdir
の対象ディレクトリが存在する場合エラーが発生しますが、exist_ok
引数を使用することでエラーを発生させないようにすることができます。
tests_path.mkdir()
> FileExistsError: [WinError 183] 既に存在するファイルを作成することはできません。
tests_path.mkdir(exist_ok=True)
# 既に存在しているディレクトリを生成しようとしても何も起こりません。
parents
もうひとつの引数はparents
引数です。これにより生成するパスの中間にあるディレクトリが存在しない場合、そのディレクトリを自動的に生成してくれるようになります。
new_path = project_path / "new" / "dir"
new_path.mkdir()
# FileNotFoundError: [WinError 3] 指定されたパスが見つかりません。 # newというディレクトリが存在しないためエラーが発生します。
new_path.mkdir(parents=True)
# c:\pathlib_demo\new\dirが生成されます。
pathlibのいまいちなところ
ここまで紹介したように非常に便利なpathlibですが、いまいちなところもあります。
とはいえ、pathlibを使わないレベルの問題はないです。
削除できるのは空のディレクトリのみ
rmdir
メソッドを使用することで空のディレクトリであれば削除することができます。
しかし、削除対象のディレクトリ配下に何かデータがある場合はエラーが発生します。
(data_path / "230201").rmdir() # 230201の配下にはファイルが2つ格納されている
# OSError: [WinError 145] ディレクトリが空ではありません。
空ではないディレクトリを削除する場合はshutil.rmtree()
を使用します。
import shutil
shutil.rmtree(data_path / "230201")
# 230201が配下のファイルごと削除されます
使用できないライブラリがある
すべてのライブラリでpathlib.Path
オブジェクトを使用できるわけではありません。
例えばpillow
ではファイルを指定するときにpathlib.Path
オブジェクトを渡すことができますが、openCV
ではエラーが発生します。
from PIL import Image
img_path = project_path / "img.jpg"
img = Image.open(img_path)
# エラー発生無し
import cv2
img = cv2.imread(img_path)
# TypeError: Can't convert object to 'str' for 'filename'
公式ドキュメントやdocstring
を読めば渡すことができる、できないの判断はできます。
pathlib.Path
オブジェクトを渡すことができないライブラリでも、組み込み関数のstr()
で文字列に変換することで渡すことができます。
img = cv2.imread(str(img_path))
IDEでパスの候補が表示されない
私はIDEとして普段VSCodeを使用しています。文字列であればパスを入力するときに拡張機能Path Autocomplete
を使うことで簡単に入力ができますが、pathlib
の記法の場合はそれが効かなくなります。効率やタイポ対策の面を考えると少し不便です。
おわりに
今回紹介できませんでしたが、pathlib
には他にも便利なメソッドが多くあります。慣れればメリットが盛り沢山なのでぜひお使い下さい。