やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Pythonにおけるファイルパス操作が難しい2

前回の続き。

問題

前回Pythonでのパス操作は、os, os.path, pathlib, 複数のモジュールを駆使せねばならないことが判明した。なんて面倒なんだ!

たとえば、以下のようなことができない。

n階層目の名前

/A/B/Cというパスがあったとき、先頭から二番目の名前Bがほしいとする。たとえば文字列操作だと'/A/B/C'.split('/')[2]のようにインデックスを指定すればできる。だが、os.pathpathlibでは簡単にできない。

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.abspathpathlib.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 クラス

Path クラス (System.IO)

じつに統一感のある名付け。長いけど略したり略さなかったり、略し方に統一性がなかったり、ハイフンを使っていたり使っていなかったり、などのバラツキがほぼ無い。

ファイル実体とは関係なく、文字列操作だけを対象としているクラスである点もわかりやすい。いい感じのまとめ方。

拡張メソッド

C# 3.0 以降では標準クラスにもメソッドを自由に追加できる。

拡張メソッド - C# によるプログラミング入門 | ++C++; // 未確認飛行 C