Pythonにおけるファイルパス操作が難しい2
前回の続き。
問題
前回、Pythonでのパス操作は、os
, os.path
, pathlib
, 複数のモジュールを駆使せねばならないことが判明した。なんて面倒なんだ!
たとえば、以下のようなことができない。
n階層目の名前
/A/B/C
というパスがあったとき、先頭から二番目の名前B
がほしいとする。たとえば文字列操作だと'/A/B/C'.split('/')[2]
のようにインデックスを指定すればできる。だが、os.path
やpathlib
では簡単にできない。
os.path.split(path)があるが、これはファイル名とディレクトリの2つに分離する関数である。使えない。
pathlib
なら一応できる。pathlib.Path('/A/B/C').parents[0].name
という非常にわかりにくい字面になってしまう。
じつはパス区切り文字はos.sep
で取得できるので、'/A/B/C'.split(os.sep)[2]
とすると一番イメージに近い。専用モジュールたちにはできない仕事である。
よくやる奴
相対パスから絶対パスを得るとか、よくやる奴の書き方。ひとつずつ見ていけば、じつに面倒であることがわかる。
コードファイル自身が存在するディレクトリパスを取得する
コード | 型 |
---|---|
pathlib.Path(__file__).resolve().parent |
pathlib.Path |
str(pathlib.Path(__file__).resolve().parent) |
str |
os.path.dirname(os.path.abspath(__file__)) |
str |
os.sep.join(os.path.abspath(__file__).split(os.sep)[:-1]) |
str |
__file__
では絶対パスが取得できない。絶対パスを取得するためにはどうしても専用API os.path.abspath
か pathlib.Path.resolve
が必要。使えない奴らかと思いきや必要。不完全でわかりにくいくせに、決して奴らへの依存なしには解決できない領域があるのが憎たらしい。
存在確認
os.path.exists('path')
pathlib.Path('path').exists()
ファイル
os.path.isfile('/tmp/a.txt')
pathlib.Path('/tmp/a.txt').is_file()
ディレクトリ
os.path.isdir('/tmp')
pathlib.Path('/tmp').is_dir()
~/
の罠
だが、こいつらには罠がある。Linuxで/home/{user}
をあらわす~/
が展開できずに存在しないと判定されてしまう。
コード | 結果 |
---|---|
os.path.isdir('~/root/') |
False |
os.path.isdir(os.path.expanduser('~/root/')) |
True |
コード | 結果 |
---|---|
pathlib.Path('~/root/').is_dir() |
False |
pathlib.Path('~/root/').expanduser().is_dir() |
True |
~
なんて、うまいことやってくれると期待するだろう。だが、裏切られる。expanduser
しないと期待どおりにならない。なぜだ? やらない理由はあるのか? やらないと困るだけでは?
渡されるパス文字列が、相対/絶対、~
か否かに関わらず、存在確認したいなら、以下のようにせねばならない。(ふつうはそれを期待すると思うが)
os.path.abspath( os.path.expanduser(path) ).isdir() pathlib.Path( path ).expanduser().resolve().is_dir()
長い。長すぎる。当前の期待を実現するために、この糞長いコードを書かねばならないだと……。
組込APIは変更できない
pathlib.Path( path ).is_dir()
で済むようにしてくれよ。いや、むしろpath.is_dir()
でたのむ。そうだ、組込のstr
にメソッド追加すればいいんだ! と思うじゃん?
しかし、Pythonでは組込オブジェクトにメソッド追加できない仕様でしたとさ \(^o^)/
def is_dir(): pass setattr(str, 'is_dir', is_dir)
TypeError: can't set attributes of built-in/extension type 'str'
privateアクセス修飾子はないくせに、こういうところでは不自由とか。使えない。一応、forbiddenfruit というパッケージがあるらしいけど、標準じゃない。
built-in object を拡張する禁断の果実を齧ろう - Qiita
相対パスでimportするモジュールを指定する
sys.path('/tmp') import Some
/tmp/Some.py
をimportするためには、/tmp
を参照するよう設定せねばならない。そこでsys.path
を使う。
import文の前に書くのでコードが汚くなるのが難点。
pathlibが使えない
sys.path.append(pathlib.Path('/tmp')) import Some
通ると思うじゃん? 残念だが通らなかった。以下のように文字列オブジェクトにすることで通った。
sys.path.append(str(pathlib.Path('/tmp'))) import Some
これはpathlib
が使えない場面があることを意味する。
さらに、pathlib.Path
オブジェクトを文字列化するためにstr()
メソッドを使うので冗長かつ可読性が下がる。
今回の場合はリテラルなので以下のようにしたほうがずっとシンプル。
sys.path.append('/tmp') import Some
結論
Pythonでパス操作するの面倒くさい。
参考
C# Path クラス
じつに統一感のある名付け。長いけど略したり略さなかったり、略し方に統一性がなかったり、ハイフンを使っていたり使っていなかったり、などのバラツキがほぼ無い。
ファイル実体とは関係なく、文字列操作だけを対象としているクラスである点もわかりやすい。いい感じのまとめ方。
拡張メソッド
C# 3.0 以降では標準クラスにもメソッドを自由に追加できる。