はじめに
Pythonでディレクトリ配下の全ファイルを1行ずつ処理するというブログ記事で、できるだけネストせずにディレクトリ配下の全ファイルを処理するというお題を取り上げていました。コードをみるとコンテキストマネージャーとイテレータを組み合わせて、見事に2段のネストで実現されていました。これを見ながら「自分だったらどう書くかな」と思いトライしてみたのがこの記事です。
試してみたこと
実装方法その1
もし自分がこれをやろうとした時に最初に思いつくのがこんな実装です。pathlib
使っているので見通しよく書けますね。
from pathlib import Path
for fpath in Path("data").iterdir():
if not fpath.is_file():
continue
with fpath.open() as fp:
for ln in fp:
print(ln.strip())
Path.iterdir()
でディレクトリ以下のファイルおよびディレクトリを拾ってきて、ファイル以外の場合はスキップ、ファイルの場合はオープンして中身を一行ずつ出力しています。
でも残念ながら3段のネストになっちゃってます。
実装方法その2
なんとか2段のネストにならないかなと思って実装したがこちら。
from pathlib import Path
for fpath in Path("data").iterdir():
if not fpath.is_file():
continue
with fpath.open() as fp:
print("".join(fp.readlines()), end="")
処理はほとんど同じですが、一行ずつ取ってくるのではなく readlines()
を使って全ての行を取ってきてそれを連結して出力しています。2段にはなりましたが、これだとそれぞれのファイルと同じだけのメモリバッファが必要になってしまいます。そして「一行ずつ取ってきて」というお題には沿っていないですね。
実装方法その3
Pathlibをそのまま使うだけではダメっぽいので、それを包む何かを作ることを考えてみます。お題を再度見てみると、「ディレクトリからファイルを取り出す」というのと「ファイルから行を取り出す」というのを繰り返せば良さそう。ということで、最初にそこを作ってしまいます。
for fpath in files_in_dir("data"):
for ln in lines_in_file(fpath):
print(ln)
うんうん、これで2段のネストの実装になっています。あとは files_in_dir()
とlines_in_file()
を実装すれば良いことになります。files_in_dir()
の方は簡単でこんな風に実装できます。
from pathlib import Path
def files_in_dir(dirname: str):
return (f for f in Path(dirname).iterdir() if f.is_file())
Path.iterdir()
の結果の中で is_file()
がTrueのものだけを返すジェネレーターを返します。
問題は lines_in_file()
の方ですが、ファイルを開くだけでなく適切に閉じなければならないところがちょっと難しい。でもこちらもyield
使ってジェネレータにしちゃえば良さそう。
def lines_in_file(fpath: Path):
with fpath.open() as fp:
for ln in fp:
yield (ln.strip())
with文でfpath.open()
しているので、このスコープを出たら開いたそのファイルは自動的に閉じられます。その途中でln.strip()
の値を返しちゃうとそのスコープから出ちゃいそうですが、yield
なのでここのコンテキストを維持したまま値を返します。そして、取り出す行がなくなって for文を抜けると withのスコープからも外れてファイルが閉じられます。
まとめ
思ったよりもスッキリ書けて満足ですが、もし「もっとこんな風に書けるぜ!」という方がいらっしゃったらぜひコメントください。