11章 ファイル

テキストファイルから読み込む

ファイルの内容を読み込む前に、まずはファイルを開かなければならない。:

組み込み関数 open() が使える。

a_file = open('examples/chinese.txt', encoding='utf-8')

Pythonには組み込みのopen()関数があり、 この関数は引数としてファイル名を受け取る。 この例でのファイル名は 'examples/chinese.txt'。 このファイル名について、興味深いことが5つある:

  • これは単なるファイル名ではなく、 ディレクトリパスとファイル名の組み合わせになっている。 ファイルを開く関数としては、ディレクトリパスとファイル名の2つの引数をとるものも考えられるが、open()関数は1つだけ受け取る。 一般にPythonで「ファイル名」が必要になる場面においては、 その中にディレクトリのパスも含めることができる。

  • このディレクトリパスにはフォワードスラッシュが使われているが、 Windowsではサブディレクトリを表すのにバックスラッシュを使う. 一方で、Mac OS XやLinuxではフォワードスラッシュを使う。 しかし、Pythonでは常に「フォワードスラッシュ /」を使えばいい。Windows上でもそれで動く。

  • このディレクトリパスはスラッシュやドライブレターから始まっていないので、 これは相対パスになる。

  • このファイル名は文字列だ。すべての現代的なオペレーティングシステム(Windowsも!)はファイル名やディレクトリ名を表すのにUnicodeを使っている。 Python 3は非asciiのパス名を完全にサポートしている。

  • このファイルがローカルディスク上にある必要はない。 コンピュータがファイルと見なし、 ファイルとしてアクセスできるものであれば、Pythonはそれを開くことができる。

文字コードが不快な姿を現す

バイトはバイトであり、文字は抽象化。 文字列はUnicode文字のシーケンス(並び)だが、 ディスク上のファイルはUnicode文字のシーケンスではなくバイトのシーケンスである。

Pythonは、特定の文字コードのエンコーディングアルゴリズムに従ってバイト列をデコードし、Unicode文字のシーケンス(つまり文字列)を返す。

In [1]:
file = open('examples/chinese.txt')
a_string = file.read()
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
<ipython-input-1-474ea7019c2e> in <module>()
      1 file = open('examples/chinese.txt')
----> 2 a_string = file.read()

UnicodeDecodeError: 'cp932' codec can't decode byte 0x82 in position 19: illegal multibyte sequence

文字コードを指定しなかったので、 Pythonはデフォルトの文字コードを使わった。

デフォルトの文字コードとは、 Tracebackをよく見てみると、 プログラムは cp932.py で停止している。 これは、ここでのデフォルトの文字コードとしてPythonがCP-932を使っていることを意味している。 CP-932文字セットはこのファイルに含まれている文字をサポートしていないので、UnicodeDecodeErrorを伴って読み込みは失敗する。

デフォルトの文字コードはプラットフォームに依存するので、 あなたのコンピュータではこのコードが問題なく動くかもしれないが(デフォルトの文字コードがutf-8の環境であればエラーは出ない)、 これを誰か他の人(CP-932などの異なるデフォルトの文字コードの環境の人)に配布したとたんに動かなくなってしまう。

ストリームオブジェクト

open()関数は、 ストリームオブジェクト と呼ばれるものを返す。 ストリームオブジェクトは、文字のストリームから情報を取得したり、 文字のストリームを操作するためのメソッドや属性を持っている。

In [2]:
 a_file = open('examples/chinese.txt', encoding='utf-8')

name属性は、 ファイルを開く際にopen()関数に渡したパス名を表している。 パスは、絶対パスへと正規化されてはいない。

In [3]:
a_file.name
Out[3]:
'examples/chinese.txt'

encoding属性は、open()関数に渡した文字コードを表している。 ファイルを開くときに文字コードを指定しなかった場合は、 locale.getpreferredencoding() の戻り値が encoding属性の値になる。

In [4]:
a_file.encoding
Out[4]:
'utf-8'
In [5]:
import locale
locale.getpreferredencoding()
Out[5]:
'cp932'

mode属性は、ファイルがどのモードで開かれたのかを教えてくれる。 open()関数にはオプションとして modeパラメータを渡すことができる。

ファイルを開く際にモードを指定しなかったので、 ここではデフォルト値の'r'がパラメータの値になっている。 この'r'は、ファイルを読み込み専用のテキストモードで開くという意味だ。 この章で後ほど見るように、ファイルモードはいくつかの目的のために使われる。 モードを使い分けることによって、ファイルに書き込んだり、 ファイルに追記したり、 ファイルをバイナリモード(ファイルを文字列ではなくバイト列として扱うモード)で開くことができる。

In [6]:
a_file.mode
Out[6]:
'r'

テキストファイルからデータを読み込む

ファイルを読み込み用に開いたら、次は、そのファイルの中身を読み込む。

いったん(正しい文字コードで)ファイルを開いてしまえば、 そこからの読み込みはストリームオブジェクトの read()メソッドを呼び出すだけ。 読み込みの結果として文字列が返される。

In [7]:
a_file = open('examples/chinese.txt', encoding='utf-8')
a_file.read()     
Out[7]:
'Dive Into Python あ\n'

再びファイルの読み込みを行っても例外は発生しない。 Pythonは、ファイルの終端 (EOF) からさらに読み込もうとしてもエラーとは見なさず、 単に空の文字列を返す。

In [8]:
a_file.read()
Out[8]:
''

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

seek()メソッドを使えば、ファイルの特定のバイト位置へ移動できる。

In [9]:
a_file.seek(0)
Out[9]:
0

read()メソッドは、オプション引数として、読み込む文字数を受け取ることができる。

In [10]:
a_file.read(16)
Out[10]:
'Dive Into Python'

1文字ずつ読み込むこともできる。

In [11]:
a_file.read(1)
Out[11]:
' '
In [12]:
a_file.read(1)
Out[12]:
'あ'

今何バイト目にいるかはtellメソッドを使う。

In [13]:
 a_file.tell()
Out[13]:
20

17バイト目に移動

In [14]:
a_file.seek(17)
Out[14]:
17

1文字読み込む。

In [15]:
a_file.read(1)
Out[15]:
'あ'

現在、20番目のバイトにいる。

In [16]:
a_file.tell()
Out[16]:
20
  • seek()tell()メソッドは常に位置をバイト単位で数える。

ファイルをテキストとして開いたので、

  • read()メソッドは文字単位で数える。

中国語・日本語の文字をutf-8 で表すには数バイトが必要になる。 ファイル中の英語の文字は1つの文字ごとに1バイトしか必要としないので、 seek()read()メソッドは同じものを数えていると勘違いしていまうかもしれない。 だが、それは一部の文字にしか当てはまらない。

  1. 18番目のバイトに移動し、1文字の読み込みを試みる。

  2. 理由は、18番目のバイトに文字がない。 最も近くにある文字は17番目のバイトから始まっている(そして3バイト続いている)。 中途半端な位置から文字を読み込もうとするとUnicodeDecodeError によって処理は失敗する。

In [17]:
a_file.seek(18)
a_file.read(1)
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
<ipython-input-17-33892425c5e3> in <module>()
      1 a_file.seek(18)
----> 2 a_file.read(1)

C:\Miniconda3\lib\codecs.py in decode(self, input, final)
    319         # decode input (taking the buffer into account)
    320         data = self.buffer + input
--> 321         (result, consumed) = self._buffer_decode(data, self.errors, final)
    322         # keep undecoded input until the next call
    323         self.buffer = data[consumed:]

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x81 in position 0: invalid start byte

ファイルを閉じる

ファイルを開くとシステムリソースが消費されるし、 ファイルモードによっては他のプログラムがそのファイルにアクセスできなくなるかもしれない。ファイルを使い終えたら、すみやかにそのファイルを閉じることが重要だ。

In [18]:
a_file.close()

ストリームオブジェクトの a_file はまだ存在している。 ストリームオブジェクトの close()メソッドを呼び出しても、 そのオブジェクト自体は破棄されない。 しかし、このオブジェクトは何の役にも立たない。

閉じたファイルから読み込むことは出来ない。 これは IOError例外を送出する。

In [19]:
a_file.read()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-19-9419ea0432b8> in <module>()
----> 1 a_file.read()

ValueError: I/O operation on closed file.

閉じたファイルをシークすることはできない。

In [20]:
 a_file.seek(0)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-20-21c0975b9185> in <module>()
----> 1 a_file.seek(0)

ValueError: I/O operation on closed file.

閉じたファイルには現在位置が存在しないので、 tell()メソッドも使えない。

In [21]:
a_file.tell()
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-21-b50dc813ecb7> in <module>()
----> 1 a_file.tell()

ValueError: I/O operation on closed file.

既に閉じられたストリームオブジェクトの close() を呼び出しても、 例外は発生しない。何も行われないだけ。

In [22]:
a_file.close()

閉じたストリームオブジェクトには、1つだけ有用な属性がある。 それは closed属性で、これによってファイルが閉じられているかが確認できる。

In [23]:
a_file.closed
Out[23]:
True

ファイルを自動的に閉じる

ストリームオブジェクトには明白な close()メソッドがあるが、 もしコードにバグがあり、close() を呼び出す前にクラッシュしてしまったら、 理論的には、ファイルが必要以上に開かれ続けることになりうる。 これはローカルコンピュータでデバッグしている時点では大した問題にならないが、 実運用するサーバ上ではおそらく問題となる。

解決策が Python 2.6 で導入されていて、 Python 3 でも、その解決策with文が使える。

以下のコードは open() を呼び出しているが、 a_file.close() の呼び出しはどこにもない。

with文は、コードブロックを開始する。 このコードブロックの中では、 定めた変数 a_file を、open() から返されたストリームオブジェクトを表すものとして使うことができる。 seek()read() など、 必要とするすべての標準のストリームオブジェクトメソッドが利用できる。 withブロックの終わりに達すると、 Pythonは a_file.close()自動的に呼び出す

In [24]:
with open('examples/chinese.txt', encoding='utf-8') as a_file:
    a_file.seek(17)
    a_character = a_file.read(1)
    print(a_character)

いつ・どのようにwithブロックを抜け出したとしても、 Pythonはそのファイルを閉じる。

たとえば、未処理例外によって「抜け出した」ときでも閉じる。 コードが例外を発生し、プログラム全体が悲鳴を上げて停止したとしても、 そのファイルは閉じられる。これは保証されている。

技術的な用語で言えば、 with文は「実行時コンテクスト」というものを生成する。 この例では、ストリームオブジェクトはコンテクストマネージャとして機能する。 Python は a_fileというストリームオブジェクトを作り、 そのオブジェクトに対して実行時コンテクストに入ることを告げる。 withコードブロックが終了すると、 Pythonはストリームオブジェクトに対して、ランタイムコンテクストから抜け出すことを告げ、 ストリームオブジェクトは自身の close()メソッドを呼び出す。

with文はファイルのためだけの構文ではない。 with文は、実行時コンテクストを作成し、 そのコンテクストへの入出をオブジェクトに伝える汎用のフレームワークにすぎない。

対象となるオブジェクトがストリームオブジェクトなら、 ファイルライクな処理(例えばファイルを自動で閉じる)が行われる。 だけれど、その振る舞いはwith文に定義されているのではなく、 ストリームオブジェクトの中で定義されている。 コンテクストマネージャは、ファイルとは関係のない様々な用途で使用できる。 独自のコンテクストマネージャを作ることもできる。

データを一行ずつ読み込む

テキストファイルの「行」というのは、いくつか単語を入力してENTERを押せば、 新しい行に入る。テキストの行は文字のシーケンスであって、 それは改行文字によって区切られている.

この改行文字とは 実のところ、これは複雑な問題で、現実には何種類もの文字が行の末尾を表す記号として使われている。 この問題については、どのOSも独自の慣習を持っている。 行の終わりにキャリッジリターン文字を使うOSもあれば、 ラインフィード文字を使うOSもあるし、この二つをまとめて行末に置くOSもある。

Pythonはデフォルトで行の末尾を自動的に処理してくれる。 「ファイルを一行ずつ読みたい」と言えば、 Pythonはテキストファイルで使われている行終端の種類を自動で判別してくれるので、 何の問題も起きない。

もし行の終端として何を使うかを細かく制御する必要がある場合は、 open()関数にオプションの newlineパラメータを渡すことができる。

ファイルを1行ずつ読む方法

1.withパターンを使い、安全にファイルを開き、Pythonに閉じさせる。

2.ファイルを1行ずつ読み込むにはforループを使えばいい。 ストリームオブジェクトは read() のような明示的なメソッドを持つだけではなく、 イテレータとしても振る舞う。 このイテレータは、値が要求されるたびに1つの行を返す。

In [25]:
line_number = 0
with open('examples/favorite-people.txt', encoding='utf-8') as a_file: # 1
    for a_line in a_file: # 2
        line_number += 1
        print('{} {}'.format(line_number, a_line.rstrip()))
    
1 Dora
2 Ethan
3 Wesley
4 John
5 Anne
6 Mike
7 Chris
8 Sarah
9 Alex
10 Lizzie

3.文字列の format()メソッドを使うことで、 行番号と行自身を出力することができる。

このフォーマット指定子{:>4}は 「4文字分のスペースに右詰めする」ことを意味する。 a_line変数は行全体を含んでおり、 それには改行文字も含まれる。

文字列メソッドの rstrip() は、改行文字を含む行末の空白を取り除く。

In [26]:
line_number = 0
with open('examples/favorite-people.txt', encoding='utf-8') as a_file:  # 1
    for a_line in a_file:                                               # 2
        line_number += 1
        print('{:>4} {}'.format(line_number, a_line.rstrip())) # 3
    
   1 Dora
   2 Ethan
   3 Wesley
   4 John
   5 Anne
   6 Mike
   7 Chris
   8 Sarah
   9 Alex
  10 Lizzie

テキストファイルに書き込む

ファイルへの書き込みは、読み込みとほぼ同じ方法で行うことができる。 まずファイルを開いてストリームオブジェクトを取得し、 次にストリームオブジェクトのメソッドを使ってファイルにデータを書き込み、 最後にファイルを閉じる。

ファイルを書き込み用に開くには、書き込みモードを指定して open()関数を呼びだす。 書き込み用のファイルモードは2種類ある:

  • 「書き込み」モードはファイルを上書きする。open()関数に mode='w' を渡す。
  • 「追記」モードはファイルの終わりにデータを追加する。 open()関数に mode='a' を渡す。

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

書き込みが終わったら、できるだけ速やかにファイルを閉じるようにする。 そうすれば、書き込んだ内容がディスクに確実に保存されるし、 ファイルハンドルも解放される。ファイルを読み込むときと同じで、ストリームオブジェクトのclose()メソッドを使うこともできるし、with文を使ってPythonに閉じさせることもできる。

新しいファイル test.log を作成し(もしくは既存のファイルを上書きし)、 そのファイルを書き込み用に開くことから始める。 mode='w'パラメータはファイルを書き込み用に開くことを意味している。 そう、これはとても危険なことだ。もしこのファイルが既に存在していたのなら、消えてしまう。

open()関数から返されたストリームオブジェクトのwrite()メソッドを使うことで、 新しく開いたファイルにデータを追加することができる。 withブロックが終わると、Pythonは自動的にファイルを閉じる。

In [27]:
with open('test.log', mode='w', encoding='utf-8') as a_file:
    a_file.write('test succeeded') 
In [28]:
with open('test.log', encoding='utf-8') as a_file:
    print(a_file.read())
test succeeded

今度はファイルへ上書きするのではなく、 mode='a' を用いてファイルに追記する。 追記によってファイルの既存の内容が破壊されることは絶対に無い。

In [29]:
with open('test.log', mode='a', encoding='utf-8') as a_file:
    a_file.write('and again')

書き込んだ元の行と、2回目に追加した行の両方が test.logファイルに含まれている。 改行(キャッリッジリターン・ラインフィード)が含まれていないことにも注意。 改行文字をファイルに明示的に書き込まなかったので、ファイルはそれを含んでいない。 キャリッジリターンは文字'\r' として書き込むことができるし、 ラインフィードは文字'\n' として書き込むことができる。 このどちらも行っていないので、ファイルに書き込んだものはすべて1行になってしまっている。

In [30]:
with open('test.log', encoding='utf-8') as a_file:
    print(a_file.read())
test succeededand again

文字コード再び

ファイルを書き込み用に開く際に、 open()関数に encodingパラメータを渡している。 これは非常に重要なので、決して省略してはならない。 この章の始めで見たように、ファイルは文字列ではなくバイト列を含んでいる。バイトのストリームを読み込んで文字列に変換するのに使う文字コードを指定したからこそ、「文字列」を読み込むことができるのだ。 テキストファイルに書き込む場合には、同じ問題が裏返しになって現れる。ファイルに文字を書き込むことはできない。文字は抽象化だからだ。 ファイルに書き込むためには、文字列をバイトのシーケンスに変換する方式を指定する必要がある。

正しい変換を行わせる唯一の方法は、ファイルを書き込み用に開くときに、 encoding パラメータを指定すること。

バイナリファイル

ファイルというのテキストばかりではない。いくつかのファイルには画像もある。

ファイルをバイナリモードで開くのはシンプルだが、違いはわずか。 テキストモードで開く場合との唯一の差は、 modeパラメータに'b' という文字を含める。

ファイルをバイナリモードで開くことによって取得したストリームオブジェクトはテキストファイルの時と同じ属性の多くを含んでいる。 その1つに mode があり、これはopen()関数に渡した modeパラメータを反映している。

In [31]:
with open('examples/beauregard.jpg', mode='rb') as a_image:
    print(a_image.mode)
rb

バイナリストリームオブジェクトは、 テキストストリームオブジェクトと同様に name属性も持っている。

In [32]:
with open('examples/beauregard.jpg', mode='rb') as a_image:
    print(a_image.name)
examples/beauregard.jpg

ここに1つの違いがある。 バイナリストリームオブジェクトには encoding属性がないのだ。 この理由はお分かりだと思う。今は文字列ではなくバイト列を読み書きしているのだから、 変換を施す必要などない。 バイナリファイルを読み込む時は、書き込まれた内容をそのまま取り出している。そこに変換処理が入り込む余地はない。

In [33]:
with open('examples/beauregard.jpg', mode='rb') as a_image:
    print(a_image.encoding)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-33-0dce65528268> in <module>()
      1 with open('examples/beauregard.jpg', mode='rb') as a_image:
----> 2     print(a_image.encoding)

AttributeError: '_io.BufferedReader' object has no attribute 'encoding'
In [34]:
with open('examples/beauregard.jpg', mode='rb') as an_image:
    print(an_image.tell())
0

文字列ではなく、バイト列を読み込んでいる。 ファイルをバイナリモードで開いたので、 read()メソッドは文字数ではなく読み込むバイト数を引数にとる。

In [35]:
with open('examples/beauregard.jpg', mode='rb') as an_image:
    data = an_image.read(3) 
    print(data)
b'\xff\xd8\xff'
In [36]:
with open('examples/beauregard.jpg', mode='rb') as an_image:
    data = an_image.read(3) 
    print(type(data))
<class 'bytes'>

これが意味するのは、 read() に渡した数と tell()メソッドが返すインデックスとのあいだには予想外の不一致が決して起き得ないということ。 read()メソッドはバイトを読み込み、seek()tell()メソッドは読み込んだバイト数を追跡する。 バイナリファイルの場合、これらは常に一致する。

In [37]:
with open('examples/beauregard.jpg', mode='rb') as an_image:
    data = an_image.read(3) 
    print(an_image.tell())
3
In [38]:
with open('examples/beauregard.jpg', mode='rb') as an_image:
    data = an_image.read() 
    print(len(data))
3150

ファイル以外をソースとするストリームオブジェクト

今、ライブラリを書いているとしよう。 そして、そのライブラリの1つの関数はファイルからデータを読み込もうとしているとする。 その関数は、「ファイル名を文字列として受け取り、そのファイルを読み込み用に開き、それを読み込み、関数を抜け出る前にそのファイルを閉じる」という単純な設計にもできるが、そうすべきではない。

その代わりに、API は任意のストリームオブジェクトを受け取るようにすべき。

最も単純な場合、 read()メソッド(オプションとしてsizeパラメータを受け取り、文字列を戻り値として返す)を持っているオブジェクトなら何だってストリームオブジェクトだといえる。 sizeパラメータなしで呼ばれた場合は、 read()メソッドは入力ソースを読み込んで、そのすべてのデータを一つの値として返さなければならない。 sizeパラメータと共に呼び出された場合は、 入力データからその分を読み込んで返す。 再度呼ばれときは、中断した場所を見つけ、その次のデータの固まりを返す。

これは実際のファイルを開いたときのストリームオブジェクトに非常によく似ている。 違いは本物のファイルだとは限らないということだ。

「読み込む」入力ソースは何でも良く、Webページや、メモリ上の文字列、他のプログラムの出力でも良い。 関数がストリームオブジェクトを受け取り、 そのオブジェクトの read()メソッドを呼び出す限り、ファイルとして振る舞ういかなる入力であっても、それらを扱うための専用のコードなしに扱うことができる。

ioモジュールは, StringIOクラスを定義しており、 これを使うと、メモリ上の文字列をあたかもファイルであるかのように扱うことができる。

In [39]:
a_string = 'PapayaWhip is the new black.'
import io 

文字列からストリームオブジェクトを作るには、 「ファイル」のデータとして扱いたい文字列を渡して io.StringIO()クラスのインスタンスを作成すればよい。 ストリームオブジェクトを手に入れたので、これを使ってストリームライクなあらゆる操作ができる。

In [40]:
a_file = io.StringIO(a_string)

read()メソッドは「ファイル」の中身を全て「読み出す」関数だが、 StringIO の場合は単純に元の文字列が返される。

In [41]:
a_file.read()
Out[41]:
'PapayaWhip is the new black.'

本物のファイルと同様に、read()メソッドをもう一度呼び出すと空の文字列が返される。

In [42]:
a_file.read()
Out[42]:
''

StringIOオブジェクトのseek()メソッドを使うことで、本物のファイルをシークする時のように、文字列の先頭に明示的にシークすることができる。

In [43]:
a_file.seek(0)
Out[43]:
0

sizeパラメータを read()メソッドに渡すことで、 文字列を小分けにして読み込むこともできる。

In [44]:
a_file.read(10) 
Out[44]:
'PapayaWhip'
In [45]:
a_file.tell()
Out[45]:
10
In [46]:
a_file.seek(18)
Out[46]:
18
In [47]:
a_file.read()
Out[47]:
'new black.'
  • io.StringIO を使うと、 文字列をテキストファイルのように扱うことができる。
  • io.BytesIOクラスというものもあり、 これを使うとバイト配列をバイナリファイルとして扱うことができる。

圧縮ファイルを扱う

Python標準ライブラリには、圧縮ファイルの読み書きをサポートするモジュールが含まれている。圧縮方式には様々な種類があるが、Windows以外のシステムにおいて最も広く使われているのはgzipとbzip2だ(PKZIPアーカイブやGNU Tarアーカイブにも出会ったことがあるかもしれない。Pythonには、これらを扱うモジュールも含まれている)。

gzipモジュールを使うと、 gzipで圧縮されたファイルを読み書きするためのストリームオブジェクトを作成できる。 このモジュールによって生成されるストリームオブジェクトはread()メソッド(読み込み用に開いた場合)や write()メソッド(書き込み用に開いた場合)をサポートしている。

つまり、解凍したデータを格納するための一時ファイルを作るなどということはせずに、通常のファイルを扱うためにすでに学んだメソッドを使って、 gzip圧縮されたファイルを直接読み書きすることができる。

さらに、このモジュールは with文もサポートしているので、gzipで圧縮されたファイルを使い終わったときに、それをPythonに自動的に閉じさせることもできる。

In [48]:
import gzip
gzip
Out[48]:
<module 'gzip' from 'C:\\Miniconda3\\lib\\gzip.py'>

gzipで圧縮されたファイルは常にバイナリモードで開かなければならない (引数modeに文字'b'が含まれることに注意)。

In [49]:
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'))

このzipファイルは、79バイトになる。 これは実のところ元の文字列よりもサイズが大きくなってしまっている。 gzipファイル形式は、ファイルのメタデータを保持する固定長のヘッダを持っているので、非常に小さいファイルに対しては非効率になる。

In [50]:
with gzip.open('out.log.gz', mode='rb') as z_file:
    print(len(z_file.read()))
52

このファイルを解凍してその内容を新しいファイルに保存する。 保存されるファイルの名前は、圧縮ファイルの名前から拡張子の .gzを取り除いたものになる。

out.log

が生成される。

標準入力・標準出力・標準エラー出力

標準出力と標準エラー出力(一般にstdout, stderrと略される)は、Mac OS XやLinuxのようなすべてのUNIXライクなシステムに標準で組み込まれているパイプだ。

  • print()関数を呼び出すと、印字しようとしたものは stdoutパイプに送られる。
  • プログラムがクラッシュしてTracebackを印字しようとしたときは、 それが stderrパイプに送られる。

デフォルトでは、作業している端末ウインドウに両方のパイプが接続されている。プログラムが何かを印字するときは、その結果を端末ウインドウで見ることになるし、プログラムがクラッシュするときは、同様にTracebackを端末ウインドウで見ることになる。

グラフィカルなPythonシェルでは、stdoutとstderrパイプはデフォルトで「対話ウインドウ」に接続されている。

今までよく使っていたループの中のprint()関数。

In [51]:
for i in range(3):
    print('PapayaWhip')
PapayaWhip
PapayaWhip
PapayaWhip

stdoutsysモジュールで定義されている。 そしてこれはストリームオブジェクトになる。 これのwrite()関数を呼び出すと、 そこに与えた文字列をなんでも表示する。

実際に、これが print関数の行っていること。 print関数は表示する文字列の末尾に改行を追加して、 sys.stdout.write を呼び出している。

In [52]:
import sys
for i in range(3):
    sys.stdout.write('is the')
is theis theis the

最も単純な場合では、 sys.stdoutsys.stderr は同じ場所、 つまりPython ide(その中にいる場合)、 もしくは端末(Pythonをコマンドラインから実行している場合)に出力する。 標準出力と同様に、標準エラー出力も改行を追加してくれない。 改行したい場合は、改行文字を書き込む必要がある。

In [53]:
for i in range(3):
    sys.stderr.write('new black')
new blacknew blacknew black

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

In [54]:
import sys
sys.stdout.read()
---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
<ipython-input-54-b249ed7d8dd2> in <module>()
      1 import sys
----> 2 sys.stdout.read()

C:\Miniconda3\lib\site-packages\ipykernel\iostream.py in read(self, size)
    298 
    299     def read(self, size=-1):
--> 300         raise IOError('Read not supported on a write only stream.')
    301 
    302     def readline(self, size=-1):

OSError: Read not supported on a write only stream.

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

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

In [55]:
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') # out.log へリダイレクトされる
print('C')
A
C
print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file):
    print('B')
print('C')

は書き換えると、

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):

with文は必ずしも as句を必要としない。 関数を呼び出して、その戻り値を無視するときのように、 withコンテクストを変数へ代入しない with文を作ることもできる。 この例では、RedirectStdoutToコンテクストの副作用だけが必要。

RedirectStdoutToクラスは、 カスタマイズされたコンテクストマネージャ

どんなクラスも2つの特殊メソッド、 __enter__()__exit__() を定義することでコンテクストマネージャになることができる。

class RedirectStdoutTo:
    def __init__(self, out_new):  # 1
        self.out_new = out_new

    def __enter__(self):          # 2 
        self.out_old = sys.stdout
        sys.stdout = self.out_new

    def __exit__(self, *args):    # 3
        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')                                                                            # 1
with open('out.log', mode='w', encoding='utf-8') as a_file, RedirectStdoutTo(a_file): # 2
    print('B')                                                                        # 3
print('C')
  1. この出力はide 「対話ウインドウ」(もしくは、スクリプトがコマンドラインで実行されているときはコマンドライン)に印字される。

  2. ここで、with文はカンマで区切られたコンテクストのリストを受け取っている。カンマで区切られたリストは、ネストされたwithブロックのように振る舞う。リストの最初のコンテクストが「外側」のブロックになり、最後のコンテクストが「内側」のコンテクストになる。最初のコンテクストはファイルを開き、次のコンテクストは、初めのコンテクストで作られたストリームオブジェクトへ sys.stdoutをリダイレクトする。

  3. このprint()関数は、with文によって作られたコンテクストと共に実行されるので、その出力は画面に印字されず、out.logファイルに書き込まれる。 ④ withコードブロックが終了した。ここでPythonは、各々のコンテクストマネージャに対して、コンテクストから抜け出るときに行うべき処理を実行するように告げる。コンテクストマネージャは後入れ先出し (LIFO) のスタックを形成している。終了の際は、2番目のコンテクストマネージャがsys.stdoutを元の値に戻し、次に1番目のコンテクストマネージャがout.logファイルを閉じる。標準出力が元の値に戻ったので、print()関数を呼び出すと再び画面上に印字されるようになる。

リダイレクトしないとどうなるか

In [56]:
print('A')
with open('out.log', mode='w', encoding='utf-8') as a_file:
    print('B') # ファイルには何も書かれずファイル書き込み終了
print('C')
A
B
C
sys.stdout = a_file # ファイルに繋ぎ変えて

sys.stdout = sys.__stdout__ # で元に戻す。

参考:標準入出力をファイルにつなぎ換える

参考リンク