step06 入出力リダイレクト、ファイルの読み書き

プログラムは普通、データを受け取って処理を行い、結果を表示する。正常に処理が行われなかった場合にはエラーメッセージも表示する。これらをまとめて、プログラムによるデータの入出力という。このstepのはじめに、入出力についてまとめておこう。

標準入出力

step02で作ったhello.py(リスト6-1に再掲する)では、キーボードから自分の名前を入力すると、コマンドプロンプト画面にあいさつ文が表示された。これが、最も簡単な入出力の形式である。

リスト6-1: hello.py(再掲)
step05ではコマンドライン引数について学び、ようやくプログラムの外からデータを渡せるようになり、それによって、より実用的なプログラムも書けるようになった。だが、プログラムの出力はつねに画面に表示されていた。
いずれの場合も、プログラム中では、直接キーボードから入力せよとか画面に出力せよとは指定してはいない。
実は、プログラムから見たとき、入出力は、具体的なデバイスに直接行われるのではなく、 と決められている。つまり、入出力先は具体的なデバイスより1段抽象化されているのだ。また、 ことも決められている。
つまり、pythonプログラムには、図6-1に示すように、データの出入口が3つある。そして、特に何も指定しなければ、 つながっている。これまでのプログラムで、特に何も気にせず、キーボードから入力し、画面に出力できていたのは、背景にこのような標準入出力の仕組みがあったからだ。
図6-1: プログラムの「データの出入口」

入出力リダイレクト

標準入出力という仕組みで、入出力先を抽象化するメリットは何だろうか。
キーボードから入力できるデータ量はたかだか数行程度だ。より多量のテキストデータを入力したければ、テキストファイルの内容を入力とするなど、別の方式を使うしかない。また、テキストではないバイナリデータを入力したい場合は、量に関係なくキーボード入力は不可能だ。
出力データの場合も同様である。たかだか数行程度の出力なら、画面に表示するだけでよいかもしれないが、より多量のテキストデータを出力する処理だと、出力結果をファイルに保存したいニーズが生ずる。
入出力先を抽象化されていなければ、入力元がキーボードかファイルか、出力先が画面かファイルかによって、同じような処理をする別々のプログラムをいくつも書くことになり、非効率的である。プログラムでは標準入出力を使い、使う場面に合わせて具体的な入出力先だけを切り替えることができれば、プログラムは1つで済む。
そのための仕組みが入出力リダイレクトである。
標準入力や標準出力をファイルにリダイレクト(向け直すこと)することで、明示的にファイルを開かなくても、入力をキーボードからではなくファイルから受け取り、出力を画面表示ではなくファイルに保存できる。この様子を図6-2に示す。

図6-2: 標準入出力のリダイレクト
同一のプログラムで入出力だけを切り替えるのであるから、具体的な入出力先の指定は、プログラム中ではなく、コマンドラインで行うことになる。したがって、入出力リダイレクトは、pythonの機能というより、Windowsコマンドプロンプトや、LinuxなどOS側の機能である。
コマンドラインからの指定方法は、以下の各セクションで示す。コマンドライン引数を併用すると、コマンドラインはやや複雑になるので、混乱しないようにしよう。

入力リダイレクト

まず入力リダイレクトについて解説する。入力データをキーボードから受け取る代わりに、ファイルから「流し込む」機能である。主としてテキストファイルに対して用いる※1

1 入出力リダイレクトの機能自体は、バイナリファイルに対しても使えるが、一般的には、バイナリファイルへの入出力は明示的にファイルをオープンして読み書きする手法が用いられる。バイナリデータはそもそも、キーボード入力や画面出力ができないのだから、わざわざ標準入出力を使う理由がない。

例として、リスト6-1に再掲したhello.pyで、自分の名前をキーボード入力する代わりに、テキストファイルから入力させてみよう。
自分の名前だけを入力したテキストファイルを作り、name.txtのファイル名で保存しておく。ファイルの末尾に改行を入力するのを忘れないこと。
コマンド行で入力をキーボードからではなくファイルname.txtから受け取るための指定は、記号「<」を用いて図6-3のように行う※2。キーボードからの入力がないので、プログラムの画面出力が崩れてしまっていることに注意。

2 漏斗のようなもので、プログラムの右側からデータを流し込むイメージか。

図6-3: 入力リダイレクトを指定するコマンド行

出力リダイレクト

つぎに出力リダイレクトについて解説する。出力データを画面に表示するのではなく、ファイルに「流し入れる」機能である。これも主としてテキストファイルについて用いる。
例として、やはりhello.pyを使おう。今度は自分の名前はキーボードから入力するが、出力されるあいさつ文を画面に表示するのではなく、テキストファイルに保存するのだ。
この場合のコマンド行は、記号「>」を用いて図6-4のように書く※3。ファイルaisatsu.txtに出力結果が保存されたことを確認しよう。

3 こちらは逆に、プログラムの右側にデータを吸い出しているイメージか。

図6-4: 出力リダイレクトを指定するコマンド行

標準エラー出力の使い方

さて、画面への出力をファイルにリダイレクトした結果、本来は画面に出力されるべきメッセージ「あなたの名前を教えてください」までもが出力ファイルに吸い取られてしまった。これではユーザと正常なインタラクションができない。また、プログラムが正常に動作できない場合(入力データが不適切なときなど)に表示するエラーメッセージも、出力リダイレクトの対象外にしなければならない。そのためにこそ、図6-2に示した3つめの標準入出力「標準エラー出力」がある。hello.pyプログラムでは、ユーザーへのメッセージも標準出力(sys.stdout)に出していたため、4行目で出力したあいさつ文と混在してしまったのだ。
そこで、リスト6-1のhello.pyプログラムを、リスト6-2のように少しだけ改造してみる。
print関数のオプション引数file = sys.stderrは、標準出力ではなく標準エラー出力に出力する指定である。2行目のimport sys文は、「sys.stderr」という名前を使うためにインポートしている。


リスト6-2: hello2.py(hello.pyの改良版)

hello2.pyを出力リダイレクトつきで実行すると、図6-5のようになる。ファイルaisatsu.txtにはあいさつ文だけが保存され、メッセージは従来通り画面に出力される。これが標準エラー出力の典型的な利用法だ。実行中に発生したエラーを通知するエラーメッセージも、できるだけ標準エラー出力に出力しよう。

図6-5: 標準エラー出力の画面表示

入出力リダイレクト構文のまとめ

コマンド行における入出力リダイレクトの構文を、これまでに紹介したものも含めて以下にまとめておく。


< in.txt	標準入力をファイルin.txtにつなぐ
> out.txt	標準出力をファイルout.txtにつなぐ
>> out.txt	標準出力をファイルout.txtに追加で書き込む

com1 | com2	com1の標準出力をcom2の標準入力につなぐ(パイプ)

2> err.txt	標準エラー出力をファイルerr.txtにつなぐ
2>> err.txt	標準エラー出力をファイルerr.txtに追加で書き込む
2>2>>はめったに使わないが、パイプについては、覚えておくと便利な機能として紹介する。

パイプ

パイプとは、複数のコマンドをつないで実行することで、より複雑で多様な作業をさせるための機能である。
例として「テキストファイルの重複した行をまとめる」タスクを考える。以下のような重複の多いテキストファイルlove.txtがある(ファイル名を右クリックし、「対象をファイルに保存」すればダウンロードできる)。
●love.txt

uniq※4コマンドを使って重複行をまとめてみる。

4 残念ながら、uniqコマンドは教室のPC環境によっては、使えない場合もある。後述のsortコマンドは、コマンドプロンプトの標準コマンドなので、どこでも使える。せっかくunixからパイプ機能を受け継ぎながら、フィルターコマンドを備えていないコマンドプロンプト(MS-DOS)の設計思想は、いささか残念だ。


uniq < love.txt
すると、標準出力には以下のテキストが表示される。

hug
kiss
hug
kiss
hug
embrass
kiss
marry
「重複が取れた」といっても、それは同じ行が並んでいる箇所だけであり、異なり行のリストがほしい場合には、目的は達せられない。これはuniqの仕様である。
しかし、行をアルファベット順(辞書順)に並べ替えるsortコマンドとパイプで組み合わせ、以下のコマンドラインを実行させれば、

sort < love.txt | uniq
標準出力には以下のテキストが表示される。

hug
kiss
embrass
marry
これで目的は達せられた。必要なら、この出力を出力リダイレクトで別のファイルに書き込めばよい。
パイプの便利さがわかっただろうか。この考え方は、標準入出力と同様、unix osに源流を持つ。
unix的なプログラミング思想は、「標準入力と標準出力を使う単機能なフィルタープログラムを多数作っておき、複雑なタスクはそれらをパイプで組み合わせて処理する」というものだ。
この思想に共感するならば、プログラムは、標準入力からのデータを処理し、標準出力に出すという、フィルタープログラムの形式をできるだけ守るべきである。

ファイル入出力

さまざまな入出力形式

pythonをはじめ、CUI環境で実行されるスクリプト言語では、たいてい入出力リダイレクト機能が利用できるので、プログラム中で明示的にファイルを指定して読み書きする必要性はあまり生じない。
だが、プログラムの入出力処理には、図6-6に示すようなさまざまなパターンがある。

図6-6: プログラムのさまざまな入出力

これらのうち、入力ファイルか出力ファイルが複数あるパターンは、入出力リダイレクトでは対応できない。また、前述のように、バイナリのファイルを入出力したいときには、標準入出力は使わず、明示的に入出力ファイルを開くのが一般的だ※5

5 バイナリデータは、キーボードからの入力も、画面への出力もできないから、入出力リダイレクトの恩恵にあずかれないのである。

pythonにおける、明示的なファイル入出力の方法を解説する。簡単な規則さえ守れば、好きなときに、いくつでも(上限はあるが)ファイル名を指定して読み書きできるので、最も汎用性の高い方法である。
指定するファイル名(またはファイルパス)は、プログラム設定のような固定的なファイル名を除けば、プログラム中に直接書き込まず、実行時に、コマンドラインから引数として指定するのが望ましい。

ファイルのオープンとクローズ

リスト6-3に、入力ファイルの各行の先頭に行番号をつけて出力するプログラムの例をを示す。ここでは、ファイルの使い方を説明するために、あえて古風な繰り返し構文(while)を使っている。


リスト6-3: lnumber.py

まず、目的のファイルをopenし、ファイルオブジェクト※6を得る。ここでは、コマンドラインで指定したファイルを読み込みモード('r')でオープンしている。


# コマンドラインで指定したファイルを開く(open)
fname = sys.argv[1]
f = open(fname, 'r')
ファイル先頭から1行ずつ読み込む処理は、ファイルオブジェクトのメソッドreadline()で行う。結果は文字列変数lineに代入されるが、ファイル末尾(EOF)に達した場合にはnullが返るので、それを検知してwhileループからbreakで抜け出している。

line = f.readline()        # 1行ずつ読み込む
if not line:
    break
自分でopenしたファイルは、使い終えたらcloseしなくてはならない。ファイルを開けっぱで放置すると、万一そのコードがループ中にある場合、じわじわとシステム資源を食い潰し、いずれシステムダウンに発展しかねない。こうした資源問題に基づくバグは、一般に極めて発見しにくいので、使ったモノはかたすという当たり前のマナーを守ろう。

# ファイルを閉じる(close)
f.close()

6 ファイルオブジェクトとは、他言語の「ファイルハンドル」に相当する、プログラム中でファイルを表すデータ構造である。for line in f:のように、行のリストと見なして繰り返し処理の対象にもできる。その知識(とその他の先進機能)を使えば、このwhileループは、リスト6-4: lnumber2.pyに示す、極めて簡潔な形に書き直せる。リスト6-3: lnumber.pyあえて古風な構文を使ったのは、それではファイルオブジェクトの使い方が分かりにくいからである。

ファイルのopenモード

ファイルをopenする際には、利用目的に応じて以下のモードを指定できる。

r
読み込みモード。ファイルがないときはエラーが発生する
w
書き込みモード。ファイルがないときは新規作成される
a
追加書き込み(append)モード。ファイルがないときは新規作成される
r+
読み書き両用モード。ファイルがないときはエラーが発生する
b
バイナリモードでopenする。以上の各モードに付加する形式で指定。例:'wb', 'r+b'

たとえば、リスト6-3のプログラム中で、出力用にもう1つファイルを開き、行番号付きの行を書き込むのなら、以下のような行を追加すればよい。


fout = open("fout.txt", 'w')		# 書き込みモードで開く

str = '{} {}'.format(n, line) # 出力文字列を作成 fout.write(str) # foutに書き込み
fout.close() # ファイルを閉じる

ファイル入出力に伴うエラー処理

ファイルを明示的で開けて入出力する処理では、エラーが発生することがある。step05ではtry~except構文によるエラー処理の基本を学んだが、ここではファイル入出力時に発生する(かもしれない)エラーの処理について解説する。
たとえばリスト6-3のプログラムで、コマンドラインで指定したファイル名のファイルが存在しなかったとする。タイプミスなどで起こりうる事態だが、このとき、プログラム中ではIOError例外が発生する。これを処理するには、リスト6-4のようにプログラムを変更する。1行ずつ読み込むためのループも、最新の方法に改めた。


リスト6-4: lnumber2.py

pythonではファイルオブジェクトリストのように扱える。つまりfor line in f:とすれば、ファイル中の各行を変数lineに代入できるのだ。ファイルを列のリストと見なしたことになる。enumerate()関数は、リストのインデックスと要素を同時に取得する関数である(第2引数の1は初期値)。
各構文や関数の定義は、教科書で確認しておこう。それを面倒がらずにやるかどうかが、確実にプログラミング上達の明暗を分ける。
終わりから2行目のelseはtry~except構文に掛かっている。つまり、「例外が発生しなかったら」の意味である。
このように、pythonのelseは、

ifを受けるelse
ifの条件式が成立しなかったら~
while, forを受けるelse
ループがbreakしなかったら~
try~exceptを受けるelse
例外が発生しなかったら~
の3通りの意味を持つ。一見ややこしいが、一度慣れれば手放せないほど便利で、洗練されたプログラムが書けることに気づく。pythonは他のプログラミング言語より後発の言語だからこそ、こうした先行言語たちのいいとこ取りができるのだ。
lnumber.pyの正常な実行結果を図6-7に示す(lnumber2.pyも同じ)。

図6-7: lnumber.pyプログラムの実行結果