ファイル操作には例外処理が付きものだ。その基本的な書き方と、with文を使った、よりシンプルな表記について見ていこう。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
前回まで、ファイルのオープン、ファイルの読み書きとクローズ、pickleモジュールとshelveモジュールによるオブジェクトのファイルへの保存、urllib.requestモジュールによるWebページの取得とBeautiful Soup 4によるそのスクレイピング、osモジュールとos.pathモジュールやpathlib.Pathクラスによるパス操作、shutilモジュールを使った高水準なファイル/ディレクトリ操作など、ファイル操作に関連する話題を取り扱ってきた。今回はファイル操作の最後の話題として、ファイル操作と例外処理、それをラップするwith文について見ていこう。
ファイル操作に例外処理は付きものだ。というのは、例えば、ユーザーが指定したファイルを読み取りモードでオープンして、その内容を読み込む関数やプログラムを作ったとする。それを使うユーザーが存在しないファイルを指定したとしたら、どうだろう。open関数にできることは、FileNotFound例外を発生させることだけだ。書き込みについても同様だ。書き込みモードで開いたファイルが何らかの理由(ファイルシステムの故障など)で突然なくなってしまったら、ファイルへの書き込み処理は失敗して例外が発生するだろう。
こうしたことから、ファイルを扱うときには例外の発生に気を配る必要がある。そこで、まずはファイルを扱うサンプルを作ってから、その例外処理について考えてみよう。サンプルとしてはテキストファイルの内容を読み込んで返すだけの関数とする。
def get_content(src):
fsrc = open(src)
content = fsrc.read()
return content
この関数はパラメーターsrcに渡された名前のファイルをオープンして、その内容を読み込んで、それを戻り値として、呼び出し側に返送する。実際に使ってみると次のようになる。
from pathlib import Path
Path('foo.txt').write_text('foo, bar, baz')
content = get_content('foo.txt')
print(content)
実行結果を以下に示す。
ただし、この関数に存在しないファイルを指定すれば、そこで例外が発生するだろう(もちろん、そこで例外が発生するのは正しいし、それに対して行えることはユーザーに「そんなファイルはない」とメッセージを伝える以外にない)。
get_content('nonexist.txt')
上のコードは、存在しないnonexist.txtファイルをget_content関数の引数としたものだ。これを実行すれば、今述べたように例外が発生する。
上の例では、ファイルのオープンで失敗しているが、その一方でファイルのオープンに成功した後の処理で例外が発生したときに忘れてはならないのは、それらのファイルを確実にクローズすることだ。これにはtry文のelse節またはfinally節が使える。上のように存在しないファイルを指定したときにはそもそもファイルがオープンされないので、それをクローズしようとすると例外が発生する。そのため、オープンしていないファイルをクローズしないようにする必要もある。
つまり、ファイルを操作するときには、実際には次のようなことに留意する必要がある。
以下ではこれらを考慮して、ファイル操作に例外処理を組み込んだコードを考えてみよう。
上で述べたようなことから、先ほどのget_content関数に例外処理を組み込むとすると、例えば次のようなコードが考えられる。
def get_content(src):
try:
fsrc = open(src)
except OSError as e:
print(e)
else:
content = fsrc.read()
fsrc.close()
return content
ここでは、ファイルオープンで例外が発生したら、そのことを画面に表示し、例外が発生しなければ、else節でその内容を読み込んで、その次にファイルをクローズして、最後に読み込んだ内容を呼び出し側に返すようにしている。try節で例外が発生した場合、ファイルはオープンされていないので、finally節でオープンしていないファイルをクローズしないように、ここではcloseメソッド呼び出しもelse節に記述している。
このget_content関数に、先ほどと同様に、存在しないファイルを指定してみよう。
get_content('nonexist.txt')
すると、次のように、例外が発生したことが告げられて、関数が終了する。もちろん、問題の解決には至っていない。だが、例外が発生したのは存在しないファイルを指定したからで、プログラム側では何もできないため、これはこれでしょうがないだろう。
あるいは、ファイル読み込み時に例外が発生することまで考慮して次のようなコードも書けるだろう。
def get_content(src):
try:
fsrc = open(src)
except OSError as e:
print(e)
else:
try:
content = fsrc.read()
return content
except Exception as e:
print(e)
finally:
fsrc.close()
こちらでは、ファイルがオープンできたことが分かっているので、2つ目の(大外のtry文のelse節に書いた)try文のfinally節でcloseメソッドを呼び出すようにしている(2つ目のtry文のelse節に書いてもよいだろう)。
そこで、ファイルのオープンに成功して、else節に書いたtry節の内部でエラーが発生したらどうなるかを確認してみよう。そのためには、上のコードを次のように書き換えてみる。2つ目のtry節で、raise文により例外発生をシミュレートすると共にfinally節でファイルがクローズされるかを確認するためにprint関数呼び出しを付加している。
def get_content(src):
try:
fsrc = open(src)
except OSError as e:
print(e)
else:
try:
content = fsrc.read()
raise Exception('hello from nested try statement')
return content
except Exception as e:
print(e)
finally:
print('closing fsrc')
fsrc.close()
この関数を定義して、以下のように今度は実際に存在するファイルを渡してやると、どうなるだろう。
get_content('foo.txt')
このコードを実行すると、ファイルのオープンには成功するので、else節にあるtry文が実行され、そのtry節にあるraise文で例外が発生する。これはその直下のexcept節で処理されて、最後にfinally節が実行される。そのため、「hello from nested try statement」とメッセージが表示されてから、次に「closing fsrc」と表示されるはずだ。実際の実行結果を以下に示す。
このように、try文を適切に記述することで、うまく例外を処理できるようになる。しかし、ここまでのコードの変遷を見ていると、わずか3行だったget_content関数の本体が、例外処理を組み込もうとした途端に、大量のコードになることが分かる。そこで、このようなtry〜except〜else〜finally形式の例外処理をシンプルに書くための機構がPythonにはある。それが次に見るwith文だ。
with文は、Pythonが持つ「コンテキストマネジャー」と呼ばれる機構を利用して、何らかの「前処理」、プログラマーが本当にしたい「中間処理」、リソースの解放など最後に必要になる「後処理」の記述をシンプルに行えるようにしたものだ。
例えば、ファイルを操作するときには、「前処理」としてファイルのオープンが必要になる。その後、「中間処理」として、自分が行いたい処理を書き、それが終わったら「後処理」としてファイルをクローズする。「オープン→自分が行いたい処理→クローズ」という定型的な処理の最初の部分(前処理)と最後の部分(後処理)の面倒を見てくれるのが、with文といえる。簡単にいえば、with文でファイルをオープンしたら、ファイルのクローズはwith文の終了時に自動的に行われるということだ(closeメソッドを呼び出す必要がなくなる)。
ファイル操作に関していえば、with文の構文は次のようになる。
with open('ファイル名') as 変数:
オープンしたファイルに対して行いたい処理
「変数」の部分には「f = open('ファイル名')」のようにopen関数呼び出しの結果を代入していた変数を書く。
このようにすることで、「open('ファイル名')」でオープンしたファイルはwith文本体に書いた「オープンしたファイルに対して行いたい処理」が終了して、次の文に進む前に、確実にクローズされるようになる。with文の中でtry文を使ってさらに例外処理を書くときにelse節やfinally節にファイルをクローズする処理を記述する必要もなくなるし、何よりファイルのクローズを忘れることがなくなるという面でもうれしい機能だ。
先ほどのコードであれば、次のように書けるだろう。
def get_content(src):
with open(src) as fsrc:
try:
content = fsrc.read()
return content
except Exception as e:
print(e)
大外のtry〜except〜else(〜finally)という構造がなくなり、スッキリとしたコードになった。ただし、違うところもある。以前のget_content関数では存在しないファイルをオープンしようとすると例外が発生するので、それをexcept節で処理していたが、上のコードではファイルオープン時に例外が発生しても、それは処理しないようになっている。
with文は「with」に続く式――例えば、open関数呼び出し――が成功した場合に、中間処理の実行時に例外が発生しても、後処理――例えば、ファイルのクローズ――が行われることを保証するものであって、「with」に続く式の実行時に発生する例外は考慮の対象外である。元のコードと同じ意味にするのであれば、with文をtry文で囲めばよいだろう。
Copyright© Digital Advantage Corp. All Rights Reserved.