検索
連載

[Python入門]ファイル操作と例外処理Python入門(2/2 ページ)

ファイル操作には例外処理が付きものだ。その基本的な書き方と、with文を使った、よりシンプルな表記について見ていこう。

PC用表示 関連情報
Share
Tweet
LINE
Hatena
前のページへ |       

ファイルをコピーする関数に例外処理を組み込む

 次にもう少し難しい例としてファイルをコピーする関数を考えてみよう。元のコードは以下の通りだ(もちろん、ファイルをコピーするだけなら、前回に見た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()

ファイルをコピーするfile_copy関数

 この関数はパラメーター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()

ファイルコピーに例外処理を組み込んだ例1

 これは最初の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()

例外処理を組み込んだfile_copy関数の例

 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)

例外処理を組み込んだfile_copy関数の例

 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)

例外処理を組み込んだfile_copy関数の例

 with文では「with」に続けて、カンマ区切りで複数の式を並べてもよい。そして、それらの式の数だけ、with文がネストしているものとして見なされる。よって、上のコードは以前のコードと同じことを意味しながら、インデントの幅が少なくなり、コード自体も読みやすくなっている。

まとめ

 今回はファイル操作にはつきものの例外処理について見た。try文による例外処理はファイル操作にまつわるエラーに対処するには必須だが、コードが複雑になる傾向にある。with文はopen関数だけではなく、本連載でこれまでに見てきたurllib.request.urlopen関数や、pathlib.Path.openメソッドなどでも利用できること、try文を使うよりもコードがシンプルに記述できるようになること、何よりクローズ処理を忘れることなく確実に行えるようになることから、積極的に使っていきたい機能である。

今回のまとめ

  • ファイル操作ではさまざまなタイミングで例外が発生する
  • そのため、try文による例外処理が必須といえる
  • try文で例外処理を記述すると、コードが複雑になることがよくある
  • with文を使うことで、そうしたコードを簡潔に記述できるようになる

Copyright© Digital Advantage Corp. All Rights Reserved.

前のページへ |       
[an error occurred while processing this directive]
ページトップに戻る