なぜpath.joinでなくpathlibを使うべきか

pathlibをos.pathモジュールを不必要にオブジェクト指向にしたものとおもってませんか?

そんなあなたにpathlibがいかに便利か布教したい!

この記事を読んで、Pythonでファイルを扱うときはいつでもPythonのpathlibモジュールを使うようになってもらえればと思います。

os.path

os.pathモジュールは、Pythonでパスを扱うために昔から利用されてきました。なので必要なものはほとんど揃っています。

ただ書き方が複雑なんです。

たとえばos.path.joinを使った場合、

import os.path

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATES_DIR = os.path.join(BASE_DIR, 'templates')
from os.path import abspath, dirname, join

BASE_DIR = dirname(dirname(abspath(__file__)))
TEMPLATES_DIR = join(BASE_DIR, 'templates')

なんて書き方をすることになります。

見にくくありませんか?

あとos.pathでのやり方はpathがあくまでstringであり、 たまたまpathのフォーマットを持っているに過ぎません。

Path専用のオブジェクトではないのです。

os.pathの文字列を入力したり出力したりする関数は、コードを内側から読まなければならないため、入れ子になっていると実に面倒です。 このような入れ子になった関数呼び出しを、連鎖したメソッド呼び出しなんて嫌じゃないですか?

その問題を解決する鍵がpathlibモジュールです。pathlibを使えば以下のように書けます

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES_DIR = BASE_DIR.joinpath('templates')

os.pathモジュールでは、関数のネストが必要でしたが、pathlibモジュールのPathクラスでは、Pathオブジェクトのメソッドや属性を連鎖させて、同等のパス表現を得ることができる。

osモジュールは機能が多くてややこしい

Python の os.path モジュールは、パスを扱う他にも様々な機能がある。パスを使って実際に何かをしたい(例えば、ディレクトリを作成したい)と思ったら、他のPythonモジュール、多くの場合osモジュールを使う必要があるんです。

osモジュールには、ファイルやディレクトリを操作するための多くのユーティリティがあります: mkdir, getcwd, chmod, stat, remove, rename, rmdir. また、chdir, link, walk, listdir, makedirs, renames, removedirs, unlink (remove と同じ), symlink までもあります。また、ファイルシステムとは全く関係のないものとして、fork、getenv、putenv、environ、getlogin、systemなど、ここでは説明できないほどたくさんあります。

pathlibモジュールは、これらのファイルシステム関連のosユーティリティの多くを、Pathオブジェクトのメソッドに置き換えることですっきり整理されてるのも魅力の一つです。

以下は、src/pypackagesディレクトリを作成し、.editorconfigファイルの名前をsrc/.editorconfigに変更するコードです。

import os
import os.path

os.makedirs(os.path.join('src', '__pypackages__'), exist_ok=True)
os.rename('.editorconfig', os.path.join('src', '.editorconfig'))

pathlibを使えば以下のようにオブジェクトのメソッドで表現することができます

from pathlib import Path

Path('src/__pypackages__').mkdir(parents=True, exist_ok=True)
Path('.editorconfig').rename('src/.editorconfig')

pathlibのコードでは、メソッドを連鎖させるためにパスが最初に置かれていることに注目し欲しい。

Zen of Pythonが言うように、「名前空間は素晴らしいアイデアのひとつであり、もっと多くのことをしよう」。osモジュールは非常に大きな名前空間で、その中にはたくさんのものが入っています。pathlib.Pathクラスは、osモジュールよりもはるかに小さく、より具体的な名前空間です。さらに、このPath名前空間のメソッドはPathオブジェクトを返すので、ネストした文字列の多い関数呼び出しの代わりに、メソッドの連鎖が可能になります。

https://inventwithpython.com/blog/2018/08/17/the-zen-of-python-explained/

globもシンプルに

Python標準ライブラリのファイルパス/ファイルシステム関連のユーティリティは、osモジュールとos.pathモジュールだけではありません。globという便利なパス関連モジュールもあります。

glob.glob関数を使って、あるパターンにマッチするファイルを見つけることができます。

toplevel = glob('*.csv')
all_csv_files = glob('**/*.csv', recursive=True)

さてこれをpathlibで表現するとこんな感じになります。

from pathlib import Path

toplevel = Path.cwd().glob('*.csv')
all_csv_files = Path.cwd().rglob('*.csv')

pathlibでファイルを開く

pathlibモジュールは、多くの複雑なケースをやや単純化しますが、単純なケースをさらに単純化するものもあります。

1つまたは複数のファイルに含まれるすべてのテキストを読む必要がありますか?

ファイルを開いて、その内容を読み、withブロックを使ってファイルを閉じることができます。

from glob import glob

file_contents= [].
for filename in glob('**/*.py', recursive=True):
    with open(filename) as python_file:
        file_contents.append(python_file.read())

あるいは、Pathオブジェクトのread_textメソッドとリスト内包を使って、ファイルの内容を1行で新しいリストに読み込むこともできます。

from pathlib import Path

file_contents = [
    path.read_text()
    for path in Path.cwd().rglob('*.py')
]

ファイルに書き込む必要がある場合は open context managerを使います

with open('.editorconfig') as config:
    config.write('# config goes here')

あるいは、write_textメソッドを使うこともできます。

Path('.editorconfig').write_text('# config goes here')

コンテキストマネージャーなどでopenを使用したい場合は、代わりにPathオブジェクトのopenメソッドを使用することができます。

from pathlib import Path

path = Path('.editorconfig')
with path.open(mode='wt') as config:
    config.write('# config goes here')

ちなみに、Python3.6では、組み込みのopen関数にPathオブジェクトを渡すこともで来ます

from pathlib import Path

path = Path('.editorconfig')
with open(path, mode='wt') as config:
    config.write('# config goes here')

Object志向でコードをより明確に

オブジェクト指向的にはJSONオブジェクトはディクショナリーにデシリアライズされ、日付はdatetime.date オブジェクトを使ってネイティブに表現され、ファイルシステムのパスはpathlib.Pathオブジェクトを使ってジェネリックに表現されるようになるととても美しです。

Pathオブジェクトを使用すると、コードがより明確になります。日付を表現しようとしているのであれば、日付オブジェクトを使うことができます。ファイルパスを表現しようとすれば、Pathオブジェクトを使うことができます。

オブジェクト指向プログラミングはクラスは別の抽象化のレイヤーを追加しますし、抽象化は時にシンプルさよりも複雑さを増すことがあります。しかし、pathlib.Pathクラスは便利な抽象化です。また、すぐに普遍的な抽象化として認識されるようになっています。

PEP 519 のおかげで、ファイルパスオブジェクトはパスを扱うための標準となりつつあります。Python 3.6 では、組み込みの open 関数や os, shutil, os.path モジュールの様々な関数が pathlib.Path オブジェクトで適切に動作します。パスを扱うコードのほとんどを変更することなく、今すぐpathlibを使い始めることができます。

pathlibには何が足りないの?

pathlibは素晴らしいものですが、完璧ではない。

例えば、pathlib.Pathメソッドの中にshutilと同等のものがない。

ファイルやディレクトリをコピー/削除/移動するための高レベルのshutil関数にPathオブジェクト(およびpath-likeオブジェクト)を渡すことはできますが、Pathオブジェクトにはこれらの関数に相当するものがありません。

そのため、ファイルをコピーするには、次のようにしなければなりません。

from pathlib import Path
from shutil import copyfile


from= Path('old_file.txt')
tofile = Path('new_file.txt')
copyfile(from, tofile)

また、pathlibにはos.chdirに相当するものがないのも残念だ。

つまり、現在の作業ディレクトリを変更する必要がある場合には、chdirをインポートする必要があります。

from pathlib import Path
from os import chdir

parent = Path('..')
chdir(parent)

os.walk関数もpathlibに相当するものはありません。しかし、pathlibを使って独自のwalkのような関数を簡単に作ることができます。

pathlib.Pathオブジェクトに、これらの欠落している操作のいくつかのメソッドが含まれるようになることを期待しています。しかし、これらの欠落した機能があったとしても、私は「os.path and friends」よりも「pathlib and friends」を使う方がはるかに扱いやすいと感じています。

常にpathlibを使うべきか? Python 3.6以降、pathlib.Pathオブジェクトは、すでにパス文字列を使用しているほぼすべての場所で動作します。ですから、もしあなたが Python 3.6 (またはそれ以上) を使っているなら、pathlib を使わない理由はありません。

もしあなたがPython3の以前のバージョンを使っているなら、文字列の世界に戻るためのエスケープハッチが必要なときに、いつでもPathオブジェクトをstrコールで包んで文字列を取り出すことができる。少々面倒ですが、以下のようになります。

from os import chdir
from Pathlib import Path

chdir(Path('/home/sasaki'))  # Python 3.6+ で動作
chdir(str(Path('/home/sasaki')) # それ以前のバージョンでも動作します。

Python 3のどのバージョンを使っているかに関わらず、pathlibを試してみることをお勧めします。

私は、pathlibを使うことでコードがより読みやすくなると感じています。ファイルを扱う私のコードのほとんどは、デフォルトでpathlibを使うようになっていますし、あなたもそうすることをお勧めします。もしpathlibを使えるなら、使うべきです。