[Python入門]ファイル操作と例外処理:Python入門(2/2 ページ)
ファイル操作には例外処理が付きものだ。その基本的な書き方と、with文を使った、よりシンプルな表記について見ていこう。
ファイルをコピーする関数に例外処理を組み込む
次にもう少し難しい例としてファイルをコピーする関数を考えてみよう。元のコードは以下の通りだ(もちろん、ファイルをコピーするだけなら、前回に見たshutil.copy関数などを使うのがよい。これはあくまでもサンプルだ)。
def file_copy(src, dst):
fsrc = open(src, 'rb')
fdst = open(dst, 'wb')
content = fsrc.read()
fdst.write(content)
fsrc.close()
fdst.close()
この関数はパラメーターsrcが指すコピー元ファイルを読み込みモードで開き(fsrcに代入)、パラメーターsrcが指すコピー先ファイルを書き込みモードで開いて(fdstに代入)、コピー元ファイルの内容を読み込んだ後に、それをコピー先のファイルに書き込み、最後に2つのファイルをクローズするという処理を行う。まだ、例外のことは考えていない。
実行例を以下に示す。
file_copy('foo.txt', 'bar.txt')
print(Path('bar.txt').read_text())
これを実行すると、次のようにコピーできていることが分かる。
このfile_copy関数に例外処理を組み込むとするとどうなるだろう。まずはtry文を素直に使ったコードを考えてみよう。例えば次のようなコードが考えられる。
def file_copy(src, dst):
try:
fsrc = open(src, 'rb')
fdst = open(dst, 'wb')
except OSError as e:
print(e)
else:
try:
content = fsrc.read()
fdst.write(content)
except Exception as e:
print(e)
finally:
fsrc.close()
fdst.close()
これは最初のtry節でコピー元とコピー先の2つのファイルをオープンして、それらをオープンできたら、else節の中で実際のコピー処理を行おうというものだ。そして、最後に大外のfinally節で2つのファイルをクローズする。
実際には、このコードはあまりよろしくない。最初のtry節で、どちらかのファイルをオープンできなかった場合に、finally節でオープンできなかったファイルまでクローズしようとしてしまうからだ(オープンできなかった時点で、そのopen関数呼び出しの結果が変数fsrc、fdstのいずれかに代入されないので、その名前は定義されないので、変数を参照しようとしたところで例外が発生する)。普通はしないことだが、以下のコードを実行してみると分かる。
file_copy('foo.txt', '') # コピー先のファイル名に空文字列を指定
このコードを実行すると、次のようになる。
「No such file or directory」というのは、except節で例外を処理したときに表示されたものだ。そして、その下でさらにUnboundLocalError例外が発生したことが分かる。変数fsrcにはオープンされたファイルを参照するオブジェクトが代入されたが、変数fdstには(ファイルがオープンされなかったので)何も代入されず、定義もされなかった。そのため、「fdstに代入される前に、その値が参照された」というメッセージが表示されたわけだ。これはfile_copy関数の第1引数に存在しないファイルを指定したときも同様だ。
というわけで、try節にopen関数呼び出しを2つ並べるのはやめて、open関数呼び出しごとにtry節を記述するのがよいだろう。実際にコードにしたのが以下だ(クローズ処理が行われるかを確認するためにprint関数呼び出しを含めている)。
def file_copy(src, dst):
try:
fsrc = open(src, 'rb')
except OSError as e:
print(e)
else:
try:
fdst = open(dst, 'wb')
except OSError as e:
print(e)
else:
try:
content = fsrc.read()
fdst.write(content)
except Exception as e:
print(e)
finally:
print('closing fsrc and fdst')
fsrc.close()
fdst.close()
finally:
print('closing fsrc')
fsrc.close()
get_content関数と同様に、こちらのコードもかなりの分量になったことが分かる。
このfile_copy関数では、最初のtry文でコピー元のファイルをオープンしようとして、それが成功したら、そのelse節に2つ目のtry文を書き、その中でコピー先のファイルをオープンしようとしている。それが成功したら2つ目のtry文のelse節に3つ目のtry文を記述して、実際のコピーを行っている。
このとき、3つ目のtry文では2つのファイルがオープンされていることが分かっているので、対応するfinally節でそれらをクローズしている。2つ目のtry文ではコピー元のファイルがオープンされていることが分かっているので、これに対応するfinally節ではコピー元のファイルだけをクローズしている。最終的に「fsrc.close()」呼び出しは2回呼び出されるが、一度オープンしたファイルに対してcloseメソッドを複数回呼び出すことは問題ない。ただし、実際にクローズ処理が行われるのは最初の呼び出しだけとなる。
この関数を定義し、「file_copy('foo.txt', '')」のように実行すると次のようになる。
この場合は、コピー先のファイル名が空文字列であるため、ファイルをオープンできなかった。そのため、2つ目のtry節で例外が発生して、その旨が表示された後、対応するfinally節でコピー元のファイルのクローズだけが行われている。
興味のある方は、適切なファイル名を与えて、ファイルクローズがどんな順序で行われるかを確認したり、3つ目のtry節で例外を発生させるようにコードを修正して(先ほどと同様に、「raise Exception('メッセージ')」行を追加すればよい)、ファイルコピー時に発生した例外がうまく処理されて、ファイルがクローズされるかも確認したりしてみてほしい。
ここで見たように、複数のファイルを扱うとコードはさらに複雑になる。これをwith文を使って書き直すと次のようになる(ここでは、with文をさらにtry文で囲むことで、ファイルオープン時の例外を処理するようにしている)。
def file_copy(src, dst):
try:
with open(src, 'rb') as fsrc:
with open(dst, 'wb') as fdst:
try:
content = fsrc.read()
fdst.write(content)
except Exception as e:
print(e)
except OSError as e:
print(e)
with文をネストさせることで、先ほどはかなりスッキリとしたコードになった。だが、このコードはさらにシンプルにできる。
def file_copy(src, dst):
try:
with open(src, 'rb') as fsrc, open(dst, 'wb') as fdst:
try:
content = fsrc.read()
fdst.write(content)
except Exception as e:
print(e)
except OSError as e:
print(e)
with文では「with」に続けて、カンマ区切りで複数の式を並べてもよい。そして、それらの式の数だけ、with文がネストしているものとして見なされる。よって、上のコードは以前のコードと同じことを意味しながら、インデントの幅が少なくなり、コード自体も読みやすくなっている。
まとめ
今回はファイル操作にはつきものの例外処理について見た。try文による例外処理はファイル操作にまつわるエラーに対処するには必須だが、コードが複雑になる傾向にある。with文はopen関数だけではなく、本連載でこれまでに見てきたurllib.request.urlopen関数や、pathlib.Path.openメソッドなどでも利用できること、try文を使うよりもコードがシンプルに記述できるようになること、何よりクローズ処理を忘れることなく確実に行えるようになることから、積極的に使っていきたい機能である。
今回のまとめ
- ファイル操作ではさまざまなタイミングで例外が発生する
- そのため、try文による例外処理が必須といえる
- try文で例外処理を記述すると、コードが複雑になることがよくある
- with文を使うことで、そうしたコードを簡潔に記述できるようになる
Copyright© Digital Advantage Corp. All Rights Reserved.