現在地: ホーム 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つある:

  1. これは単なるファイル名ではなく、ディレクトリパスとファイル名の組み合わせになっている。ファイルを開く関数としては、ディレクトリパスとファイル名の2つの引数をとるものも考えられるが、open()関数は1つだけ受け取る。一般にPythonで「ファイル名」が必要になる場面においては、その中にディレクトリのパスも含めることができる。
  2. このディレクトリパスにはフォワードスラッシュが使われているが、私がこの例で何のOSを使っているかについて言っていなかった。Windowsではサブディレクトリを表すのにバックスラッシュを使う一方で、Mac OS XやLinuxではフォワードスラッシュを使う。しかし、Pythonでは常にフォワードスラッシュを使えばいい。Windows上でもそれで動く。
  3. このディレクトリパスはスラッシュやドライブレターから始まっていないので、これは相対パスと呼ばれる。何に対して相対なの?と聞きたいかもしれない。おちつきなさい、バッタさん。
  4. このファイル名は文字列だ。すべての現代的なオペレーティングシステム(Windowsも!)はファイル名やディレクトリ名を表すのにUnicodeを使っている。Python 3は非ASCIIのパス名を完全にサポートしている。
  5. このファイルがローカルディスク上にある必要はない。ネットワークドライブをマウントしているかもしれないし、そのファイルは完全に仮想的なファイルシステムが作り出す幻影かもしれない。コンピュータがファイルと見なし、ファイルとしてアクセスできるものであれば、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'
  1. name属性は、ファイルを開く際にopen()関数に渡したパス名を表している。パスは、絶対パスへと正規化されてはいない。
  2. 同じようにencoding属性は、open()関数に渡した文字コードを表している。ファイルを開くときに文字コードを指定しなかった場合は(ダメな開発者だ!)、locale.getpreferredencoding()の戻り値がencoding属性の値になる。
  3. 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()                                            
''
  1. いったん(正しい文字コードで)ファイルを開いてしまえば、そこからの読み込みはストリームオブジェクトのread()メソッドを呼び出すだけだ。読み込みの結果として文字列が返される。
  2. 少し意外かもしれないが、再びファイルの読み込みを行っても例外は発生しない。Pythonは、ファイルの終端 (EOF) からさらに読み込もうとしてもエラーとは見なさず、単に空の文字列を返すだけなのだ。

ファイルを再び読み込みたいときはどうすればいいのだろうか?

# 前の例から続く
>>> 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
  1. まだファイルの終わりにいるので、ストリームオブジェクトのread()メソッドをさらに呼び出しても空の文字列が返されるだけだ。
  2. seek()メソッドを使えば、ファイルの特定のバイト位置へ移動できる。
  3. read()メソッドは、オプション引数として、読み込む文字数を受け取ることができる。
  4. お望みなら、1文字ずつ読み込むこともできる。
  5. 16 + 1 + 1 = …… 20 ?

もう一度やってみよう。

# 前の例から続く
>>> a_file.seek(17)                    
17
>>> a_file.read(1)                     
'是'
>>> a_file.tell()                      
20
  1. 17番目のバイトへ移動する。
  2. 1文字読み込む。
  3. 現在、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
  1. 18番目のバイトに移動し、1文字の読み込みを試みる。
  2. これがどうしてエラーになるのだろう? 理由は、18番目のバイトに文字がないからだ。最も近くにある文字は17番目のバイトから始まっている(そして3バイト続いている)。中途半端な位置から文字を読み込もうとすると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
  1. 閉じたファイルから読み込むことは出来ない。これはIOError例外を送出する。
  2. 閉じたファイルをシークすることはできない。
  3. 閉じたファイルには現在位置が存在しないので、tell()メソッドも使えない。
  4. 意外かもしれないが、既に閉じられたストリームオブジェクトのclose()を呼び出しても、例外は発生しない。何も行われないだけだ。
  5. 閉じたストリームオブジェクトには、1つだけ有用な属性がある。それは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行ずつ読む方法のことだ。これは実に簡単で、そして美しい。

[oneline.pyをダウンロードする]

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()))          
  1. withパターンを使い、安全にファイルを開き、Pythonに閉じさせる。
  2. ファイルを1行ずつ読み込むにはforループを使えばいい。それだけだ。ストリームオブジェクトはread()のような明示的なメソッドを持つだけではなく、イテレータとしても振る舞うのだ。このイテレータは、値が要求されるたびに1つの行を返す。
  3. 文字列の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種類ある:

どちらのモードであっても、ファイルがまだ存在していない場合にはファイルを自動的に作成するので、「初回でも開けるようにファイルがまだ存在しない場合には空のファイルを作成する関数」のような厄介なものは決して必要ない。ファイルを開いて書き始めるだけでいい。

書き込みが終わったら、できるだけ速やかにファイルを閉じるようにしよう。そうすれば、書き込んだ内容がディスクに確実に保存されるし、ファイルハンドルも解放される。ファイルを読み込むときと同じで、ストリームオブジェクトの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                                           
  1. 新しいファイルtest.logを大胆に作成し(もしくは既存のファイルを上書きし)、そのファイルを書き込み用に開くことから始める。mode='w'パラメータはファイルを書き込み用に開くことを意味している。そう、これはとても危険なことだ。もしこのファイルが既に存在していたのなら、その内容を気にかけていなかったことを祈りたい。そのデータはもう消えてしまったからね。
  2. open()関数から返されたストリームオブジェクトのwrite()メソッドを使うことで、新しく開いたファイルにデータを追加することができる。withブロックが終わると、Pythonは自動的にファイルを閉じる。
  3. 面白かったので、もう一度やってみよう。しかし、今度はファイルへ上書きするのではなく、mode='a'を用いてファイルに追記しよう。追記によってファイルの既存の内容が破壊されることは絶対に無い
  4. 書き込んだ元の行と、2回目に追加した行の両方が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'
  1. ファイルをバイナリモードで開くのはシンプルだが、違いはわずかだ。テキストモードで開く場合との唯一の差は、modeパラメータに'b'という文字を含めることだ。
  2. ファイルをバイナリモードで開くことによって取得したストリームオブジェクトはテキストファイルの時と同じ属性の多くを含んでいる。その1つにmodeがあり、これはopen()関数に渡したmodeパラメータを反映している。
  3. バイナリストリームオブジェクトは、テキストストリームオブジェクトと同様にname属性も持っている。
  4. ここに1つの違いがある。バイナリストリームオブジェクトには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
  1. テキストファイルと同様に、バイナリファイルを少しずつ読み込むことができる。しかし、ここには重大な違いがあり……
  2. ……文字列ではなく、バイト列を読み込んでいるのだ。ファイルをバイナリモードで開いたので、read()メソッドは文字数ではなく読み込むバイト数を引数にとる。
  3. これが意味するのは、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.'
  1. ioモジュールはStringIOクラスを定義しており、これを使うと、メモリ上の文字列をあたかもファイルであるかのように扱うことができる。
  2. 文字列からストリームオブジェクトを作るには、「ファイル」のデータとして扱いたい文字列を渡してio.StringIO()クラスのインスタンスを作成すればよい。ストリームオブジェクトを手に入れたので、これを使ってストリームライクなあらゆる操作ができる。
  3. read()メソッドは「ファイル」の中身を全て「読み出す」関数だが、StringIOの場合は単純に元の文字列が返される。
  4. 本物のファイルと同様に、read()メソッドをもう一度呼び出すと空の文字列が返される。
  5. StringIOオブジェクトのseek()メソッドを使うことで、本物のファイルをシークする時のように、文字列の先頭に明示的にシークすることができる。
  6. sizeパラメータをread()メソッドに渡すことで、文字列を小分けにして読み込むこともできる。

io.StringIOを使うと、文字列をテキストファイルのように扱うことができる。またio.BytesIOクラスというものもあり、これを使うとバイト配列をバイナリファイルとして扱うことができる。

圧縮ファイルを扱う

Python標準ライブラリには、圧縮ファイルの読み書きをサポートするモジュールが含まれている。圧縮方式には様々な種類があるが、Windows以外のシステムにおいて最も広く使われているのはgzipbzip2だ(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.
  1. gzipで圧縮されたファイルは常にバイナリモードで開かなければならない(引数modeに文字'b'が含まれることに注意しよう)。
  2. 私はこの例をLinux上で作成した。コマンドラインに詳しくない人のために説明すると、このコマンドはPythonシェルで作成したgzip圧縮ファイルを「長い形式で一覧表示」している。この一覧表示は、ファイルがちゃんと存在しており(素晴らしい)、その長さが79バイトだということを示している。これは実のところ元の文字列よりもサイズが大きくなってしまっている! gzipファイル形式は、ファイルのメタデータを保持する固定長のヘッダを持っているので、非常に小さいファイルに対しては非効率なのだ。
  3. gunzipコマンド(「ジー・アンジップ」と発音する)は、ファイルを解凍してその内容を新しいファイルに保存する。保存されるファイルの名前は、圧縮ファイルの名前から拡張子の.gzを取り除いたものになる。
  4. 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シェルでは、stdoutstderrパイプはデフォルトで「対話ウインドウ」に接続されている。

>>> 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
  1. ループの中のprint()関数だ。ここには驚くようなことはない。
  2. stdoutsysモジュールで定義されている。そしてこれはストリームオブジェクトだ。これのwrite()関数を呼び出すと、そこに与えた文字列をなんでも表示する。実際に、これがprint関数の行っていることだ。print関数は表示する文字列の末尾に改行を追加して、sys.stdout.writeを呼び出す。
  3. 最も単純な場合では、sys.stdoutsys.stderrは同じ場所、つまりPython IDE(その中にいる場合)、もしくは端末(Pythonをコマンドラインから実行している場合)に出力する。標準出力と同様に、標準エラー出力も改行を追加してくれない。改行したい場合は、改行文字を書き込む必要がある。

sys.stdoutsys.stderrはストリームオブジェクトだが、これらは書き込み専用だ。これらのread()メソッドを呼び出そうとすると、常にIOErrorが発生する。

>>> import sys
>>> sys.stdout.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IOError: not readable

標準出力をリダイレクトする

sys.stdoutsys.stderrは、書き込みのみをサポートするストリームオブジェクトだ。しかし、これらは定数ではなく変数だ。これが意味するのは、新しい値、つまり任意のストリームオブジェクトを代入することで、これらの出力先をリダイレクトできるということだ。

[stdout.pyをダウンロードする]

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
  1. __init__()メソッドはインスタンスが生成された直後に呼び出される。ここでは1つの引数として、このコンテクストが存在する間だけ標準入力として使いたいストリームオブジェクトを受け取る。このメソッドはストリームオブジェクトが後で他のメソッドから使えるように、インスタンス変数に保存するだけだ。
  2. __enter__()メソッドは特殊クラスメソッドだ。Pythonは、コンテクストに入るとき(つまりwith文の初め)にこのメソッドを呼び出す。このメソッドは、sys.stdoutの現在の値をself.out_oldに保存し、次にself.out_newsys.stdoutに代入することによって標準出力をリダイレクトする。
  3. __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')                                                                             
  1. この出力はIDE 「対話ウインドウ」(もしくは、スクリプトがコマンドラインで実行されているときはコマンドライン)に印字される。
  2. ここで、withカンマで区切られたコンテクストのリストを受け取っている。カンマで区切られたリストは、ネストされたwithブロックのように振る舞う。リストの最初のコンテクストが「外側」のブロックになり、最後のコンテクストが「内側」のコンテクストになる。最初のコンテクストはファイルを開き、次のコンテクストは、初めのコンテクストで作られたストリームオブジェクトへsys.stdoutをリダイレクトする。
  3. このprint()関数は、with文によって作られたコンテクストと共に実行されるので、その出力は画面に印字されず、out.logファイルに書き込まれる。
  4. withコードブロックが終了した。ここでPythonは、各々のコンテクストマネージャに対して、コンテクストから抜け出るときに行うべき処理を実行するように告げる。コンテクストマネージャは後入れ先出し (LIFO) のスタックを形成している。終了の際は、2番目のコンテクストマネージャがsys.stdoutを元の値に戻し、次に1番目のコンテクストマネージャがout.logファイルを閉じる。標準出力が元の値に戻ったので、print()関数を呼び出すと再び画面上に印字されるようになる。

標準エラー出力のリダイレクトも全く同じ方法で行える。sys.stdoutの代わりにsys.stderrを使えばいい。

もっと知りたい人のために

© 2001– Mark Pilgrim
© Fukada, Fujimoto(日本語版)