前編では基本的な例外処理の手法、および例外の種類について扱いました。中編では、

  • 例外処理の場合分け
  • どのような例外が発生しているかの確認
  • 例外をわざと発生させるテクニック

について解説していきます。

例外処理の場合分け

前回は、try/exceptについて学びました。簡単に復習すると、例外が発生する可能性のある箇所を「try」で囲み、例外が発生した場合の処理を「except」に書くのでした。

たとえば以下のプログラムを実行するとします。

print('1: outside of try/catch')

try:
    print('2: inside of try scope')
    5 / 0
    print('3: inside of try scope')
except Exception:
    print('4: inside of except(catch) scope')

print('5: outside of try/catch')

このとき、tryの中にある5/0で例外が発生し、その例外がexceptでキャッチされるので、出力は以下のようになります。

# python test.py
1: outside of try/catch
2: inside of try scope
4: inside of except(catch) scope
5: outside of try/catch

5/0で例外が発生しており、tryの中での処理は打ち切られてしまうため、'3: inside of try scope'の出力はありませんね。また、この5/0をコメントアウトすると、出力は以下のように変わります。

python test.py
1: outside of try/catch
2: inside of try scope
3: inside of try scope
5: outside of try/catch

'3: inside of try scope'が表示されるようになった代わりに'4: inside of except(catch) scope'が表示されなくなっています。これは、tryの中で例外が発生しないとexceptが呼び出されないためです。

今回はこれに加えてelseとfinallyを扱います。それぞれtryやexceptと同じような構文を持っており、その役割を簡単に説明すると、

  • else: 例外が発生しなかった場合のみ処理される
  • finally: 例外が発生してもしなくても処理される

となります。

正直なところ、try/exceptに比べると使いどころはあまり多くないのですが、elseは例外が発生しなかった場合の処理に使い、finallyは必ず実施したい処理がある場合に使います。使いどころがなかなか難しく、私は「あえてこの処理をします」という表明の目的以外では両者を使いません。

たとえば、オープンしたファイルをfinallyでクローズするということは言語を問わずよく実施されますが、try/catchを抜けた箇所でのクローズでもだいたいカバーできます。ただ、finallyにあえてクローズ処理を書くことで、「このtry/catchでファイルの資源を開放することを保証します」ということが、ほかの人にも伝わるようになります。

少しコードを書いて実験してみましょうか。基本は前回と同じですが、今回はelseとfinallyが追加されています。

try:
    print('1: start of try')
    5 / 0
    print('2: end of try')

except Exception:
    print('3: error happens')

else:
    print("4: error doesn't happen")

finally:
    print('5: finally')

注目して欲しいのは、例外が発生したあとにどの処理が実行されているかということです。とりあえず実行してみます。

# python test.py
1: start of try
3: error happens
5: finally

例外が発生して、exceptの処理とfinallyの処理が実行されていることがわかります。 次に、例外を発生させなくした場合です。0による除算をコメントアウトします。

try:
    print('1: start of try')
    # 5 / 0
    print('2: end of try')

except Exception:
    print('3: error happens')

else:
    print("4: error doesn't happen")

finally:
    print('5: finally')

これを実行すると以下のようになります。

# python test.py
1: start of try
2: end of try
4: error doesn't happen
5: finally

今度はexceptが呼ばれなくなり、代わりにelseが呼ばれています。一方、finallyはまたもや呼ばれています。それほど複雑な動きではないと思うので、elseとfinallyの話はこのあたりで打ち切ります。なお、finallyでファイルをクローズする以下のコードは、

try:
    f = open('hello.txt', 'r')
    for line in f:
        print(line),

except:
    print('error')

finally:
    f.close()

finnalyを使わなくても、以下のように書くことができます。

try:
    f = open('hello.txt', 'r')
    for line in f:
        print(line),

except:
    print('error')

f.close()

これと同じようにelseも、わざわざ作らなくても「try の最後の場所に書く」ことでエラーが発生していないときだけ実行されますので、厳密に処理としてelseが必要という場合でなければ積極的に使うというものではないかもしれません。

例外処理をしつつ例外の内容を把握する

try/exceptで例外をキャッチできることはわかりました。ただ、やみくもに例外をキャッチするのは正直なところあまり行儀はよくないので、「正しくエラーをキャッチ」してあげる必要があります。たとえば、以下のコードがあるとしましょう。

try:
    5/0
    a = [1,2,3]
    print(a[3])

except Exception:
    print('error happens')

このコードではいつものように5/0でエラーが起きますが、それをコメントアウトしてもその後のリストの範囲外へのアクセスでエラーが発生します。このコードには問題があるためエラーの発生自体は仕方がないのですが、このコードで問題となるのは、

  • はたして0除算とリストの範囲外へのアクセスを同等に扱うべきか
  • そもそもこれは「例外処理」で対処するのではなくバグ修正すべきでは

といったことです。

今回のような短いコードであればすぐに問題は見つかるかもしれませんが、実際にはもっと長く複雑なコードが、ひとつのtryのカバー範囲になります。そのため「何が原因でどこでどのようなエラーが発生したのか」を正確に把握し、可能な限り例外処理ではなくエラーの根本原因を潰していくべきです。これをしっかり把握できれば、「何を修正すべき」か、「どのような例外クラスを使うべきか」がよくわかります。

実はこの原因と問題の把握は、try/catchを利用していなければできていました。たとえば上記コードのtry/catchを外して実行すると、5/0を実行した際に処理の中断とともに以下のようなエラー出力が得られてどこで何が発生していたのか一目でわかります。

python test.py
Traceback (most recent call last):
  File "test.py", line 1, in <module>
    5/0
ZeroDivisionError: integer division or modulo by zero

また、a[3]での範囲外のアクセスも、

python test.py
Traceback (most recent call last):
  File "test.py", line 3, in <module>
    print(a[3])
IndexError: list index out of range

というように、リストの範囲外アクセスによるエラーを起こしていることがわかります。このエラー出力を「try/catch の処理中」に得られれば、処理の中断なしに問題原因の把握、および修正ができるようになります。これを実現するためにはstack traceと呼ばれている「エラーの発生までの流れ」をtry/catchの中で得る必要があります。それにはいくつかの手法があるので順番に紹介します。

traceback モジュール

今までのコードに手を加えずに実行できるのが、tracebackモジュールを使うという方法です。簡単なのでコードを見てみましょう。なお、exceptの後に何も例外クラスを書いていませんが、このようにするとExceptionを指定したのと同じ動きをします。

import traceback

print(1)

try:
    5/0
except:
    print(traceback.format_exc())

print(2)

インポートしたtracebackのformat_exc()関数を呼び出すことで、エラーの内容を表示しています。また、エラーもtry/catchで囲まれており、例外処理が働いているので、プログラムは中断されておらず、最後のprint(2)も実行されます。

1
Traceback (most recent call last):
  File "test.py", line 6, in <module>
    5/0
ZeroDivisionError: integer division or modulo by zero

2

このtracebackモジュールは多機能なので、より詳細な使い方については以下のドキュメントをご確認ください。

例外の処理を変数に格納する

前回お話ししたように、try/catchのexceptはエラーの内容に応じたものが呼び出されます。具体的にはExcept、IOErrorなどがありましたね。この例外処理の宣言時に変数を宣言することで、その例外を変数に格納することができます。これもそれほど難しくないのでコードで確認してみます。

import traceback

print(1)

try:
    5/0
except Exception, e:
    print(type(e))
    print(e)

print(2)

exceptの後に、Exceptionに続けてeがありますね。このeが例外を格納している変数です。eは実際にはException型ではなく「Exceptionおよびその子クラスのいずれか」が格納されていますので、typeとともにその内容を表示させています。さっそく、実行させて出力を見てみましょう。

1
<type 'exceptions.ZeroDivisionError'>
integer division or modulo by zero
2

ZeroDivisionErrorが発生していることがわかりますね。printさせれば、なぜZeroDivisionErrorになったかがわかります。

なお、このexceptは以下のように書くこともできます。

except Exception as e:
    print(type(e))
    print(e)

次回は例外処理の後編となります。

  • 意図的に例外を発生させる方法
  • 例外を呼び出し元でハンドルする
  • カスタム例外クラスの作成
  • assertを使ったテスト
  • withとas

を扱います。

執筆者紹介

伊藤裕一(ITO Yuichi)

シスコシステムズでの業務と大学での研究活動でコンピュータネットワークに6年関わる。専門はL2/L3 Switching とデータセンター関連技術およびSDN。TACとしてシスコ顧客のテクニカルサポート業務に従事。社内向けのソフトウェア関連のトレーニングおよびデータセンタとSDN関係の外部講演なども行う。

もともと仮想ネットワーク関連技術の研究開発に従事していたこともあり、ネットワークだけでなくプログラミングやLinux関連技術にも精通。Cisco社内外向けのトラブルシューティングツールの開発や、趣味で音声合成処理のアプリケーションやサービスを開発。

Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009年度 IPA 未踏プロジェクト採択

詳細(英語)はこちら