現在地: ホーム ‣ Dive Into Python 3 ‣
難易度: ♦♦♦♢♢
❝ 9マイルも歩くのは楽じゃない、雨の中はなおさらだ。❞
— ハリイ・ケメルマン、The Nine Mile Walk
私のWindowsラップトップには、アプリケーションを何も入れていない段階ですでに38,493個のファイルがあった。Python 3をインストールすると、そこにおよそ3,000個のファイルが追加された。主要なオペレーティングシステムのどれにおいても、ファイルはデータを格納するための最も基本的な枠組みになっている。この概念はとても深く根付いているので、ほとんどの人にはこれとは別の枠組みを想像することすら難しいと思う。皆さんのコンピュータは、例えて言えば、大量のファイルの中で溺れているのだ。
ファイルの内容を読み込む前に、まずはファイルを開かなければならない。Pythonでファイルを開くのはすごく簡単だ:
a_file = open('examples/chinese.txt', encoding='utf-8')
Pythonには組み込みのopen()
関数があり、この関数は引数としてファイル名を受け取る。この例でのファイル名は'examples/chinese.txt'
だ。このファイル名について、興味深いことが5つある:
open()
関数は1つだけ受け取る。一般にPythonで「ファイル名」が必要になる場面においては、その中にディレクトリのパスも含めることができる。
しかしopen()
関数の呼び出しはファイル名だけで終わっていない。もう一つencoding
という引数がある。おやおや、これはうんざりするほど聞き覚えのあるものじゃないか。
バイトはバイトであり、文字は抽象化だ。文字列はUnicode文字のシーケンス(並び)だが、ディスク上のファイルはUnicode文字のシーケンスではなくバイトのシーケンスである。だとすると、「テキストファイル」をディスクから読むときに、Pythonはどのようにしてバイトのシーケンスを文字のシーケンスに変換すればよいのだろうか? Pythonは、特定の文字コードのエンコーディングアルゴリズムに従ってバイト列をデコードし、Unicode文字のシーケンス(つまり文字列)を返すのだ。
# この例はWindows上で作成した。以下に理由を簡単に示すが、 # ほかのプラットフォーム上では、異なる振る舞いをするかもしれない。 >>> file = open('examples/chinese.txt') >>> a_string = file.read() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Python31\lib\encodings\cp1252.py", line 23, in decode return codecs.charmap_decode(input,self.errors,decoding_table)[0] UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f in position 28: character maps to <undefined> >>>
一体何が起きたのだろうか? 文字コードを指定しなかったので、Pythonはデフォルトの文字コードを使わざるを得なかったのだ。デフォルトの文字コードとは何だろう? Tracebackをよく見てみると、プログラムはcp1252.py
で停止していることが分かる。これは、ここでのデフォルトの文字コードとしてPythonがCP-1252を使っていることを意味している(CP-1252は西欧言語のWindowsマシンで一般的な文字コードの1つだ)。CP-1252文字セットはこのファイルに含まれている文字をサポートしていないので、不快なUnicodeDecodeError
を伴って読み込みは失敗する。
ちょっと待って欲しい、問題はもっと深刻だ! デフォルトの文字コードはプラットフォームに依存するので、あなたのコンピュータではこのコードが問題なく動くかもしれないが(デフォルトの文字コードがUTF-8の環境であればエラーは出ない)、これを誰か他の人(CP-1252などの異なるデフォルトの文字コードの環境の人)に配布したとたんに動かなくなってしまうのだ。
☞デフォルトの文字コードを取得したければ、
locale
モジュールをインポートし、locale.getpreferredencoding()
を呼び出せばいい。私のWindowsラップトップでは、この関数は'cp1252'
を返したが、上の階にあるLinuxマシンでは'UTF8'
を返した。私の家の中でさえ一貫していないのだ! このデフォルトの値はOSのバージョンや地域・言語設定によっても変わりうる(Windowsでも異なる場合がある)。これこそが、ファイルを開くときに文字コードを必ず指定することが重要な理由だ。
今までのところ分かったのは、open()
という組み込み関数がPythonに存在するということだけだ。このopen()
関数は、ストリームオブジェクトと呼ばれるものを返す。ストリームオブジェクトは、文字のストリームから情報を取得したり、文字のストリームを操作するためのメソッドや属性を持っている。
>>> a_file = open('examples/chinese.txt', encoding='utf-8') >>> a_file.name ① 'examples/chinese.txt' >>> a_file.encoding ② 'utf-8' >>> a_file.mode ③ 'r'
name
属性は、ファイルを開く際にopen()
関数に渡したパス名を表している。パスは、絶対パスへと正規化されてはいない。
encoding
属性は、open()
関数に渡した文字コードを表している。ファイルを開くときに文字コードを指定しなかった場合は(ダメな開発者だ!)、locale.getpreferredencoding()
の戻り値がencoding
属性の値になる。
mode
属性は、ファイルがどのモードで開かれたのかを教えてくれる。open()
関数にはオプションとしてmodeパラメータを渡すことができる。ファイルを開く際にモードを指定しなかったので、ここではデフォルト値の'r'
がパラメータの値になっている。この'r'
は、ファイルを読み込み専用のテキストモードで開くという意味だ。この章で後ほど見るように、ファイルモードはいくつかの目的のために使われる。モードを使い分けることによって、ファイルに書き込んだり、ファイルに追記したり、ファイルをバイナリモード(ファイルを文字列ではなくバイト列として扱うモード)で開くことができる。
☞
open()
関数のドキュメントにすべてのファイルモードの一覧がある。
ファイルを読み込み用に開いたら、次は、そのファイルの中身を読み込むということになるだろう。
>>> a_file = open('examples/chinese.txt', encoding='utf-8') >>> a_file.read() ① 'Dive Into Python 是为有经验的程序员编写的一本 Python 书。\n' >>> a_file.read() ② ''
read()
メソッドを呼び出すだけだ。読み込みの結果として文字列が返される。
ファイルを再び読み込みたいときはどうすればいいのだろうか?
# 前の例から続く >>> a_file.read() ① '' >>> a_file.seek(0) ② 0 >>> a_file.read(16) ③ 'Dive Into Python' >>> a_file.read(1) ④ ' ' >>> a_file.read(1) '是' >>> a_file.tell() ⑤ 20
read()
メソッドをさらに呼び出しても空の文字列が返されるだけだ。
seek()
メソッドを使えば、ファイルの特定のバイト位置へ移動できる。
read()
メソッドは、オプション引数として、読み込む文字数を受け取ることができる。
もう一度やってみよう。
# 前の例から続く >>> a_file.seek(17) ① 17 >>> a_file.read(1) ② '是' >>> a_file.tell() ③ 20
もうお分かりだろうか? seek()
とtell()
メソッドは常に位置をバイト単位で数えるが、ファイルをテキストとして開いたので、read()
メソッドは文字単位で数えるのだ。中国語の文字をUTF-8で表すには数バイトが必要になる。ファイル中の英語の文字は1つの文字ごとに1バイトしか必要としないので、seek()
とread()
メソッドは同じものを数えていると勘違いしていまうかもしれない。だが、それは一部の文字にしか当てはまらないのだ。
ちょっと待って、もっと悪いことがある!
>>> a_file.seek(18) ① 18 >>> a_file.read(1) ② Traceback (most recent call last): File "<pyshell#12>", line 1, in <module> a_file.read(1) File "C:\Python31\lib\codecs.py", line 300, in decode (result, consumed) = self._buffer_decode(data, self.errors, final) UnicodeDecodeError: 'utf8' codec can't decode byte 0x98 in position 0: unexpected code byte
UnicodeDecodeError
によって処理は失敗する。
ファイルを開くとシステムリソースが消費されるし、ファイルモードによっては他のプログラムがそのファイルにアクセスできなくなるかもしれない。ファイルを使い終えたら、すみやかにそのファイルを閉じることが重要だ。
# 前の例から続く >>> a_file.close()
これだけ。なんだか拍子抜けだ。
ストリームオブジェクトのa_fileはまだ存在している。ストリームオブジェクトのclose()
メソッドを呼び出しても、そのオブジェクト自体は破棄されないのだ。しかし、このオブジェクトは何の役にも立たない。
# 前の例から続く >>> a_file.read() ① Traceback (most recent call last): File "<pyshell#24>", line 1, in <module> a_file.read() ValueError: I/O operation on closed file. >>> a_file.seek(0) ② Traceback (most recent call last): File "<pyshell#25>", line 1, in <module> a_file.seek(0) ValueError: I/O operation on closed file. >>> a_file.tell() ③ Traceback (most recent call last): File "<pyshell#26>", line 1, in <module> a_file.tell() ValueError: I/O operation on closed file. >>> a_file.close() ④ >>> a_file.closed ⑤ True
IOError
例外を送出する。
tell()
メソッドも使えない。
close()
を呼び出しても、例外は発生しない。何も行われないだけだ。
closed
属性で、これによってファイルが閉じられているかが確認できる。
ストリームオブジェクトには明白なclose()
メソッドがあるが、もしコードにバグがあり、close()
を呼び出す前にクラッシュしてしまったら何が起きるのだろうか? 理論的には、ファイルが必要以上に開かれ続けることになりうる。これはローカルコンピュータでデバッグしている時点では大した問題にならないが、実運用するサーバ上ではおそらく問題となるだろう。
Python 2にはこれを解決する方法が1つあった: try..finally
ブロックだ。これはPython 3でも使えるので、他人のコードやPython 3へ移植されたコードで見かけるかもしれない。しかし、もっと明快な解決策がPython 2.6で導入されていて、Python 3ではそちらを使うほうが望ましい。その解決策とはwith
文だ。
with open('examples/chinese.txt', encoding='utf-8') as a_file:
a_file.seek(17)
a_character = a_file.read(1)
print(a_character)
このコードはopen()
を呼び出しているが、a_file.close()
の呼び出しはどこにもない。with
文は、if
文やfor
ループと同じようにコードブロックを開始する。このコードブロックの中では、変数a_fileを、open()
から返されたストリームオブジェクトを表すものとして使うことができる。seek()
やread()
など、必要とするすべての標準のストリームオブジェクトメソッドが利用できる。with
ブロックの終わりに達すると、Pythonはa_file.close()
を自動的に呼び出す。
重要なのは次の点だ: いつ・どのようにwith
ブロックを抜け出したとしても、Pythonはそのファイルを閉じる……たとえ未処理例外によって「抜け出した」ときでも閉じるのだ。そう、コードが例外を発生し、プログラム全体が悲鳴を上げて停止したとしても、そのファイルは閉じられる。これは保証されているのだ。
☞技術的な用語で言えば、
with
文は実行時コンテクストというものを生成する。この例では、ストリームオブジェクトはコンテクストマネージャとして機能する。Pythonはa_fileというストリームオブジェクトを作り、そのオブジェクトに対して実行時コンテクストに入ることを告げる。with
コードブロックが終了すると、Pythonはストリームオブジェクトに対して、ランタイムコンテクストから抜け出すことを告げ、ストリームオブジェクトは自身のclose()
メソッドを呼び出す。詳細はAppendix B, 「with
ブロックで使用できるクラス」を参照してほしい。
with
文はファイルのためだけの構文ではない。with
文は、実行時コンテクストを作成し、そのコンテクストへの入出をオブジェクトに伝える汎用のフレームワークにすぎないのだ。対象となるオブジェクトがストリームオブジェクトなら、ファイルライクな処理(例えばファイルを自動で閉じる)が行われるだろう。だけれど、その振る舞いはwith
文に定義されているのではなく、ストリームオブジェクトの中で定義されているのだ。コンテクストマネージャは、ファイルとは関係のない様々な用途で使用できる。この章で後ほどで見るように、独自のコンテクストマネージャを作ることもできる。
テキストファイルの「行」というのは、皆さんが考えている通りのものだ — いくつか単語を入力してENTERを押せば、新しい行に入る。テキストの行は文字のシーケンスであって、それは改行文字によって区切られている……だけれど、この改行文字とは一体何のことだろう? 実のところ、これは複雑な問題で、現実には何種類もの文字が行の末尾を表す記号として使われている。この問題については、どのOSも独自の慣習を持っている。行の終わりにキャリッジリターン文字を使うOSもあれば、ラインフィード文字を使うOSもあるし、この二つをまとめて行末に置くOSもある。
でも安心して欲しい。Pythonはデフォルトで行の末尾を自動的に処理してくれるのだ。「ファイルを一行ずつ読みたい」と言えば、Pythonはテキストファイルで使われている行終端の種類を自動で判別してくれるので、何の問題も起きない。
☞もし行の終端として何を使うかを細かく制御する必要がある場合は、
open()
関数にオプションのnewline
パラメータを渡すことができる。これについての複雑な詳細を知りたい場合はopen()
関数のドキュメントを参照してほしい。
さて、具体的にはどうすればよいのだろうか? ファイルを1行ずつ読む方法のことだ。これは実に簡単で、そして美しい。
line_number = 0
with open('examples/favorite-people.txt', encoding='utf-8') as a_file: ①
for a_line in a_file: ②
line_number += 1
print('{:>4} {}'.format(line_number, a_line.rstrip())) ③
with
パターンを使い、安全にファイルを開き、Pythonに閉じさせる。
for
ループを使えばいい。それだけだ。ストリームオブジェクトはread()
のような明示的なメソッドを持つだけではなく、イテレータとしても振る舞うのだ。このイテレータは、値が要求されるたびに1つの行を返す。
format()
メソッドを使うことで、行番号と行自身を出力することができる。このフォーマット指定子{:>4}
は「4文字分のスペースに右詰めする」ことを意味する。a_line変数は行全体を含んでおり、それには改行文字も含まれる。文字列メソッドのrstrip()
は、改行文字を含む行末の空白を取り除く。
you@localhost:~/diveintopython3$ python3 examples/oneline.py 1 Dora 2 Ethan 3 Wesley 4 John 5 Anne 6 Mike 7 Chris 8 Sarah 9 Alex 10 Lizzie
このエラーが起きただろうか?
you@localhost:~/diveintopython3$ python3 examples/oneline.py Traceback (most recent call last): File "examples/oneline.py", line 4, in <module> print('{:>4} {}'.format(line_number, a_line.rstrip())) ValueError: zero length field name in formatもし起きたのなら、おそらくPython 3.0を使っているのだろう。Python 3.1にアップグレードして欲しい。
Python 3.0は文字列フォーマットをサポートしているが、それは明示的に番号付けされたフォーマット指定子だけに限られる。Python 3.1ではフォーマット指定子の中の引数インデックスを省略できる。比較のために、Python 3.0と互換性のあるバージョンも示しておく:
print('{0:>4} {1}'.format(line_number, a_line.rstrip()))
⁂
ファイルへの書き込みは、読み込みとほぼ同じ方法で行うことができる。まずファイルを開いてストリームオブジェクトを取得し、次にストリームオブジェクトのメソッドを使ってファイルにデータを書き込み、最後にファイルを閉じる。
ファイルを書き込み用に開くには、書き込みモードを指定してopen()
関数を呼びだす。書き込み用のファイルモードは2種類ある:
open()
関数にmode='w'
を渡そう。
open()
関数にmode='a'
を渡そう。
どちらのモードであっても、ファイルがまだ存在していない場合にはファイルを自動的に作成するので、「初回でも開けるようにファイルがまだ存在しない場合には空のファイルを作成する関数」のような厄介なものは決して必要ない。ファイルを開いて書き始めるだけでいい。
書き込みが終わったら、できるだけ速やかにファイルを閉じるようにしよう。そうすれば、書き込んだ内容がディスクに確実に保存されるし、ファイルハンドルも解放される。ファイルを読み込むときと同じで、ストリームオブジェクトのclose()
メソッドを使うこともできるし、with
文を使ってPythonに閉じさせることもできる。私がどちらを奨めるかは言う必要がないはずだ。
>>> with open('test.log', mode='w', encoding='utf-8') as a_file: ① ... a_file.write('test succeeded') ② >>> with open('test.log', encoding='utf-8') as a_file: ... print(a_file.read()) test succeeded >>> with open('test.log', mode='a', encoding='utf-8') as a_file: ③ ... a_file.write('and again') >>> with open('test.log', encoding='utf-8') as a_file: ... print(a_file.read()) test succeededand again ④
test.log
を大胆に作成し(もしくは既存のファイルを上書きし)、そのファイルを書き込み用に開くことから始める。mode='w'
パラメータはファイルを書き込み用に開くことを意味している。そう、これはとても危険なことだ。もしこのファイルが既に存在していたのなら、その内容を気にかけていなかったことを祈りたい。そのデータはもう消えてしまったからね。
open()
関数から返されたストリームオブジェクトのwrite()
メソッドを使うことで、新しく開いたファイルにデータを追加することができる。with
ブロックが終わると、Pythonは自動的にファイルを閉じる。
mode='a'
を用いてファイルに追記しよう。追記によってファイルの既存の内容が破壊されることは絶対に無い。
test.log
ファイルに含まれている。改行(キャッリッジリターン・ラインフィード)が含まれていないことにも注意しよう。改行文字をファイルに明示的に書き込まなかったので、ファイルはそれを含んでいない。キャリッジリターンは文字'\r'
として書き込むことができるし、ラインフィードは文字'\n'
として書き込むことができる。このどちらも行っていないので、ファイルに書き込んだものはすべて1行になってしまっている。
ファイルを書き込み用に開く際に、open()
関数にencoding
パラメータを渡していることに気づいただろうか? これは非常に重要なので、決して省略してはならない。この章の始めで見たように、ファイルは文字列ではなくバイト列を含んでいる。バイトのストリームを読み込んで文字列に変換するのに使う文字コードを指定したからこそ、「文字列」を読み込むことができるのだ。テキストファイルに書き込む場合には、同じ問題が裏返しになって現れる。ファイルに文字を書き込むことはできない。文字は抽象化だからだ。ファイルに書き込むためには、文字列をバイトのシーケンスに変換する方式を指定する必要がある。正しい変換を行わせる唯一の方法は、ファイルを書き込み用に開くときに、encoding
パラメータを指定することだ。
⁂
ファイルというのテキストばかりではない。いくつかのファイルには私の愛犬の写真が入っている。
>>> an_image = open('examples/beauregard.jpg', mode='rb') ① >>> an_image.mode ② 'rb' >>> an_image.name ③ 'examples/beauregard.jpg' >>> an_image.encoding ④ Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: '_io.BufferedReader' object has no attribute 'encoding'
mode
パラメータに'b'
という文字を含めることだ。
mode
があり、これはopen()
関数に渡したmode
パラメータを反映している。
name
属性も持っている。
encoding
属性がないのだ。この理由はお分かりだと思う。今は文字列ではなくバイト列を読み書きしているのだから、変換を施す必要などないのだ。バイナリファイルを読み込む時は、書き込まれた内容をそのまま取り出している。そこに変換処理が入り込む余地はない。
バイト列を読み込んでいるということを言ってあっただろうか。もちろん、バイト列を読み込んでいる。
# 前の例から続く >>> an_image.tell() 0 >>> data = an_image.read(3) ① >>> data b'\xff\xd8\xff' >>> type(data) ② <class 'bytes'> >>> an_image.tell() ③ 3 >>> an_image.seek(0) 0 >>> data = an_image.read() >>> len(data) 3150
read()
メソッドは文字数ではなく読み込むバイト数を引数にとる。
read()
に渡した数とtell()
メソッドが返すインデックスとのあいだには予想外の不一致が決して起き得ないということだ。read()
メソッドはバイトを読み込み、seek()
とtell()
メソッドは読み込んだバイト数を追跡する。バイナリファイルの場合、これらは常に一致する。
⁂
今、ライブラリを書いているとしよう。そして、そのライブラリの1つの関数はファイルからデータを読み込もうとしているとする。その関数は、「ファイル名を文字列として受け取り、そのファイルを読み込み用に開き、それを読み込み、関数を抜け出る前にそのファイルを閉じる」という単純な設計にもできるが、そうすべきではない。その代わりに、あなたのAPIは任意のストリームオブジェクトを受け取るようにすべきだ。
最も単純な場合、read()
メソッド(オプションとしてsizeパラメータを受け取り、文字列を戻り値として返す)を持っているオブジェクトなら何だってストリームオブジェクトだといえる。sizeパラメータなしで呼ばれた場合は、read()
メソッドは入力ソースを読み込んで、そのすべてのデータを一つの値として返さなければならない。sizeパラメータと共に呼び出された場合は、入力データからその分を読み込んで返す。再度呼ばれときは、中断した場所を見つけ、その次のデータの固まりを返す。
これは実際のファイルを開いたときのストリームオブジェクトに非常によく似ている。違いは本物のファイルだとは限らないということだ。「読み込む」入力ソースは何でも良く、Webページや、メモリ上の文字列、他のプログラムの出力でも良い。関数がストリームオブジェクトを受け取り、そのオブジェクトのread()
メソッドを呼び出す限り、ファイルとして振る舞ういかなる入力であっても、それらを扱うための専用のコードなしに扱うことができる。
>>> a_string = 'PapayaWhip is the new black.' >>> import io ① >>> a_file = io.StringIO(a_string) ② >>> a_file.read() ③ 'PapayaWhip is the new black.' >>> a_file.read() ④ '' >>> a_file.seek(0) ⑤ 0 >>> a_file.read(10) ⑥ 'PapayaWhip' >>> a_file.tell() 10 >>> a_file.seek(18) 18 >>> a_file.read() 'new black.'
io
モジュールはStringIO
クラスを定義しており、これを使うと、メモリ上の文字列をあたかもファイルであるかのように扱うことができる。
io.StringIO()
クラスのインスタンスを作成すればよい。ストリームオブジェクトを手に入れたので、これを使ってストリームライクなあらゆる操作ができる。
read()
メソッドは「ファイル」の中身を全て「読み出す」関数だが、StringIO
の場合は単純に元の文字列が返される。
read()
メソッドをもう一度呼び出すと空の文字列が返される。
StringIO
オブジェクトのseek()
メソッドを使うことで、本物のファイルをシークする時のように、文字列の先頭に明示的にシークすることができる。
read()
メソッドに渡すことで、文字列を小分けにして読み込むこともできる。
☞
io.StringIO
を使うと、文字列をテキストファイルのように扱うことができる。またio.BytesIO
クラスというものもあり、これを使うとバイト配列をバイナリファイルとして扱うことができる。
Python標準ライブラリには、圧縮ファイルの読み書きをサポートするモジュールが含まれている。圧縮方式には様々な種類があるが、Windows以外のシステムにおいて最も広く使われているのはgzipとbzip2だ(PKZIPアーカイブやGNU Tarアーカイブにも出会ったことがあるかもしれない。Pythonには、これらを扱うモジュールも含まれている)。
gzip
モジュールを使うと、gzipで圧縮されたファイルを読み書きするためのストリームオブジェクトを作成できる。このモジュールによって生成されるストリームオブジェクトはread()
メソッド(読み込み用に開いた場合)やwrite()
メソッド(書き込み用に開いた場合)をサポートしている。つまり、解凍したデータを格納するための一時ファイルを作るなどということはせずに、通常のファイルを扱うためにすでに学んだメソッドを使って、gzip圧縮されたファイルを直接読み書きすることができるというわけだ。
さらにおまけとして、このモジュールはwith
文もサポートしているので、gzipで圧縮されたファイルを使い終わったときに、それをPythonに自動的に閉じさせることもできる。
you@localhost:~$ python3 >>> import gzip >>> with gzip.open('out.log.gz', mode='wb') as z_file: ① ... z_file.write('A nine mile walk is no joke, especially in the rain.'.encode('utf-8')) ... >>> exit() you@localhost:~$ ls -l out.log.gz ② -rw-r--r-- 1 mark mark 79 2009-07-19 14:29 out.log.gz you@localhost:~$ gunzip out.log.gz ③ you@localhost:~$ cat out.log ④ A nine mile walk is no joke, especially in the rain.
mode
に文字'b'
が含まれることに注意しよう)。
gunzip
コマンド(「ジー・アンジップ」と発音する)は、ファイルを解凍してその内容を新しいファイルに保存する。保存されるファイルの名前は、圧縮ファイルの名前から拡張子の.gz
を取り除いたものになる。
cat
コマンドはファイルの内容を表示する。このファイルの中身は、先ほどPythonシェルから圧縮ファイルのout.log.gz
に直接書き込んだ文字列だ。
このエラーが起きただろうか?
>>> with gzip.open('out.log.gz', mode='wb') as z_file: ... z_file.write('A nine mile walk is no joke, especially in the rain.'.encode('utf-8')) ... Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'GzipFile' object has no attribute '__exit__'もし起きたのなら、おそらくPython 3.0を使っているのだろう。Python 3.1にアップグレードして欲しい。
Python 3.0にも
gzip
モジュールは存在するが、gzip圧縮されたファイルのオブジェクトをコンテクストマネージャとして使うことはできなかった。Python 3.1では、gzip圧縮されたファイルのオブジェクトをwith
文で使えるようになっている。
⁂
コマンドラインに慣れ親しんでいる人なら、標準入力・標準出力・標準エラー出力の概念について既に熟知していることだろう。この節は、それ以外の人々に向けて書かれている。
標準出力と標準エラー出力(一般にstdout
, stderr
と略される)は、Mac OS XやLinuxのようなすべてのUNIXライクなシステムに標準で組み込まれているパイプだ。print()
関数を呼び出すと、印字しようとしたものはstdout
パイプに送られる。プログラムがクラッシュしてTracebackを印字しようとしたときは、それがstderr
パイプに送られる。デフォルトでは、作業している端末ウインドウに両方のパイプが接続されている。プログラムが何かを印字するときは、その結果を端末ウインドウで見ることになるし、プログラムがクラッシュするときは、同様にTracebackを端末ウインドウで見ることになる。グラフィカルなPythonシェルでは、stdout
とstderr
パイプはデフォルトで「対話ウインドウ」に接続されている。
>>> for i in range(3): ... print('PapayaWhip') ① PapayaWhip PapayaWhip PapayaWhip >>> import sys >>> for i in range(3): ... sys.stdout.write('is the') ② is theis theis the >>> for i in range(3): ... sys.stderr.write('new black') ③ new blacknew blacknew black
print()
関数だ。ここには驚くようなことはない。
stdout
はsys
モジュールで定義されている。そしてこれはストリームオブジェクトだ。これのwrite()
関数を呼び出すと、そこに与えた文字列をなんでも表示する。実際に、これがprint
関数の行っていることだ。print
関数は表示する文字列の末尾に改行を追加して、sys.stdout.write
を呼び出す。
sys.stdout
とsys.stderr
は同じ場所、つまりPython IDE(その中にいる場合)、もしくは端末(Pythonをコマンドラインから実行している場合)に出力する。標準出力と同様に、標準エラー出力も改行を追加してくれない。改行したい場合は、改行文字を書き込む必要がある。
sys.stdout
とsys.stderr
はストリームオブジェクトだが、これらは書き込み専用だ。これらのread()
メソッドを呼び出そうとすると、常にIOError
が発生する。
>>> import sys >>> sys.stdout.read() Traceback (most recent call last): File "<stdin>", line 1, in <module> IOError: not readable
sys.stdout
とsys.stderr
は、書き込みのみをサポートするストリームオブジェクトだ。しかし、これらは定数ではなく変数だ。これが意味するのは、新しい値、つまり任意のストリームオブジェクトを代入することで、これらの出力先をリダイレクトできるということだ。
import sys
class RedirectStdoutTo:
def __init__(self, out_new):
self.out_new = out_new
def __enter__(self):
self.out_old = sys.stdout
sys.stdout = self.out_new
def __exit__(self, *args):
sys.stdout = self.out_old
print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
print('B')
print('C')
これを調べてみよう:
you@localhost:~/diveintopython3/examples$ python3 stdout.py A C you@localhost:~/diveintopython3/examples$ cat out.log B
このエラーが起きただろうか?
you@localhost:~/diveintopython3/examples$ python3 stdout.py File "stdout.py", line 15 with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file): ^ SyntaxError: invalid syntaxもし起きたのなら、おそらくPython 3.0を使っているのだろう。Python 3.1にアップグレードして欲しい。
Python 3.0は
with
文をサポートしているが、個々のwith
文は1つのコンテクストマネージャしか扱うことができない。Python 3.1では単一のwith
文のなかで複数のコンテクストマネージャを連鎖させることができる。
まずは後半部分を見てみよう。
print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
print('B')
print('C')
これは複雑なwith
文だ。もっと分かりやすい形に書き換えてみよう。
with open('out.log', mode='w', encoding='utf-8') as a_file:
with RedirectStdoutTo(a_file):
print('B')
このように書き換えてみると分かるように、これは実際には2つのwith
文からなり、一方が他方のスコープ内にネストされている。「外側」のwith
文についてはもう既によく分かっているだろう。ここではout.log
という名前のUTF-8でエンコードされたテキストファイルを書き込み用に開き、そのストリームオブジェクトをa_fileという名前の変数に代入している。しかし奇妙な点はまだある。
with RedirectStdoutTo(a_file):
as
句はどこにあるのだろうか? 実を言うと、with
文は必ずしもas
句を必要としないのだ。関数を呼び出して、その戻り値を無視するときのように、with
コンテクストを変数へ代入しないwith
文を作ることもできる。この例では、RedirectStdoutTo
コンテクストの副作用だけが必要なのだ。
その副作用とは一体何だろう? RedirectStdoutTo
クラスの中を見てみよう。このクラスはカスタマイズされたコンテクストマネージャだ。どんなクラスも2つの特殊メソッド、つまり__enter__()
と__exit__()
を定義することでコンテクストマネージャになることができる。
class RedirectStdoutTo:
def __init__(self, out_new): ①
self.out_new = out_new
def __enter__(self): ②
self.out_old = sys.stdout
sys.stdout = self.out_new
def __exit__(self, *args): ③
sys.stdout = self.out_old
__init__()
メソッドはインスタンスが生成された直後に呼び出される。ここでは1つの引数として、このコンテクストが存在する間だけ標準入力として使いたいストリームオブジェクトを受け取る。このメソッドはストリームオブジェクトが後で他のメソッドから使えるように、インスタンス変数に保存するだけだ。
__enter__()
メソッドは特殊クラスメソッドだ。Pythonは、コンテクストに入るとき(つまりwith
文の初め)にこのメソッドを呼び出す。このメソッドは、sys.stdout
の現在の値をself.out_oldに保存し、次にself.out_newをsys.stdoutに代入することによって標準出力をリダイレクトする。
__exit__()
メソッドはもう1つの特殊クラスメソッドだ。Pythonは、コンテクストから抜けるとき(つまりwith
文の最後)にこのメソッドを呼び出す。このメソッドは、保存しておいたself.out_oldの値をsys.stdoutに代入することによって標準出力を元の値に戻す。
すべてをまとめよう:
print('A') ①
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file): ②
print('B') ③
print('C') ④
with
文はカンマで区切られたコンテクストのリストを受け取っている。カンマで区切られたリストは、ネストされたwith
ブロックのように振る舞う。リストの最初のコンテクストが「外側」のブロックになり、最後のコンテクストが「内側」のコンテクストになる。最初のコンテクストはファイルを開き、次のコンテクストは、初めのコンテクストで作られたストリームオブジェクトへsys.stdout
をリダイレクトする。
print()
関数は、with
文によって作られたコンテクストと共に実行されるので、その出力は画面に印字されず、out.log
ファイルに書き込まれる。
with
コードブロックが終了した。ここでPythonは、各々のコンテクストマネージャに対して、コンテクストから抜け出るときに行うべき処理を実行するように告げる。コンテクストマネージャは後入れ先出し (LIFO) のスタックを形成している。終了の際は、2番目のコンテクストマネージャがsys.stdout
を元の値に戻し、次に1番目のコンテクストマネージャがout.log
ファイルを閉じる。標準出力が元の値に戻ったので、print()
関数を呼び出すと再び画面上に印字されるようになる。
標準エラー出力のリダイレクトも全く同じ方法で行える。sys.stdout
の代わりにsys.stderr
を使えばいい。
⁂
io
モジュール
sys.stdout
と sys.stderr
© 2001– Mark Pilgrim
© Fukada, Fujimoto(日本語版)