今回が例外処理の最後の回となります。主に以下について扱います。

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

意図的に例外を発生させる方法

まずは、意図的に例外を発生させる手法について扱います。なぜ意図的に例外を発生させる必要があるかについてなのですが、簡単に言ってしまえば「プログラムが自分の意図しない状態になりそうなら、その処理を中断する」ために使います。ちょうどCのgotoで例外処理へ飛ばすテクニックに似ています。

簡単なサンプルを見てみます。

try:
    a = 0
    if(a == 0):
        raise Exception('hello')
    else:
        5/a
except Exception as e:
    print(type(e))
    print(e)

前にお話したように0での除算は数学的にアウトなので、割る前に除数が0でないかを チェックさせています。そして、それが0であった場合はZeroDivisionErrorにさせるよりも「自分がコントロールできる形でわざとエラーとする」ことでZeroDivisionErrorとなることを避けています。それを実現するためにraiseを使って、その後ろのExceptionを発生させています。なお、Exceptionに与えられている'hello'はエラーの内容を記載しています。そのあとのexceptでこのエラーをキャッチできるので、今回はraiseされたExceptionをつかんでいます。

このプログラムの出力は以下のようになります。helloというメッセージを持つExceptionをキャッチできていますね。

<type 'exceptions.Exception'>
hello

このような処理を見て「どうせエラーになるのだから、どっちでもいいじゃないか」と思うかもしれませんが、同じエラーであってもraiseでエラーとしている場合は「エラーを意図して発生させている」ことがわかり、それを自分でコントロールできていることがわかります(まぁ、実装次第ですが……)。一方、raiseでなく5/aでzeroDivisionErrorエラーが発生し、それをexceptで扱っていると、それは「コードを読んだ人」にとっては意図せずエラーが発生しているように見えてしまいます。

つまり、たとえ実装者が期待したエラーハンドリングだと思っていても、ほかの人にはそう見えない可能性があるのです。自分でエラーをコントロールする場合はエラーが発生してしまってから対処するのではなく、エラーが発生する前に自前に対処をしてしまうようにすべきです。

必ずしもraiseを使う必要はなく、たとえば期待されない値の代わりにデフォルトの設定で動くようにしたり、場合によっては正しいアウトプットとともにプログラムを停止させてしまったり、returnで関数を中断させてしまったりなど、さまざまな対処法があります。ただ、raiseを使うべき場面というものもあるので、その存在だけは知っておいたほうがよいかもしれません。

例外を呼び出し元でハンドルする

先に説明したraiseなのですが、もうひとつ別の使い方があります。それは「exceptの中で呼び出すことで、エラーを呼び出し元に例外処理してもらう」というものです。以下のコードを見てください。

def fun1():
    try:
        raise Exception('error 1')
    except:
        print('1: fun1 can handle this error')

def fun2():
    try:
        raise Exception('error 2')
    except:
        print("2: fun2 can't handle this error")
        raise

def fun3():
    try:
        print('3: call fun1')
        fun1()
        print('4: call fun2')
        fun2()
        print('5: done')
    except Exception as e:
        print('6: catch error which happens in funX()')
        print(e)

fun3()

少し長いのですが、関数fun3()はfun1()、fun2()をその内部で呼び出しています。fun1()、fun2() はその内部でそれぞれエラー処理をしていますが、fun2()はexceptの中でraiseをしています。これが呼び出し元に処理を依頼するコードです。一方、fun1()はexceptの中でraiseをしていません。

両者にどういう違いがあるかというと、fun1()はexceptの中でエラーをすべてハンドルしている一方、fun2()は一応try/exceptでエラーをハンドルしようとしたけれども「ここでは対処しきれないエラーなので、呼び出し元で例外処理をしてもらう」という実装をしています。

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

3: call fun1
1: fun1 can handle this error
4: call fun2
2: fun2 can't handle this error
6: catch error which happens in funX()
error 2

exceptの中でraiseとだけ書くと、キャッチした例外を再度発生させます。呼び出し元のfun3でのexceptで、fun2のtryの中で発生したエラーをキャッチできていることがわかりますね。そのため、fun2ではなく、fun3でfun2の例外処理を行うことが可能です。

ちなみに、上記のfun1、fun2でtry/exceptを書かずにいきなりraiseをすることも可能です。そのようにした場合、エラーは呼び出し元であるfun3に丸投げしていることになります。適切に使うのであれば問題ないのですが、手抜きで上に丸投げしすぎるとエラーハンドリングがわかりづらくなるので注意してください。エラーハンドルは誰が読んでもわかりやすいものにすることが鉄則です。

カスタム例外クラスの作成

今までは既存の例外クラスのみを利用していましたが、自分で例外クラスを作りそれを使うことも可能です。

これはraiseとともに使うのですが、既存の例外クラスよりも「自分がわざと発生させた例外」を扱うには行儀がいいです。なぜなら既存の例外クラスは本来の利用用途があり、それは自分が意図的に発生させた例外とは異なるからです。

さっそく例外クラスを作って、使ってみます。

class MyError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

try:
    raise MyError('my error happens')
except MyError as e:
    print(type(e))
    print(e)

Exceptionクラスを継承してMyErrorクラスを作っています。名前は基本的には「なんとかError」とするのが通例です。継承の詳細は割愛しますが、正直なところ名前以外のコードは完全にコピペするか、もしくはコンストラクタの引数あたりを少しいじる程度になると思います。そしてexceptでは自分が作った例外クラスを指定してエラーをキャッチしています。エラーの名前が適切であれば、かなり行儀がいいコードだと思います。

これを実行すると以下のような出力が得られます。

<class '__main__.MyError'>
'my error happens'

基本的には既存の例外クラスとほぼ同じ使い方ですね。自分で例外クラスを作る場合でも、あまり複雑な実装をせずに名前だけで区別するのが通例のようです。

assertを使ったテスト

次にassertについて扱います。assertはテストを実施するための特別な例外で、基本的にはraiseと似ています。ただ、独自の文法をもっていて「特定条件を満たす場合のみ例外を発生させる」という場合に便利です。

コードで確認してみます。

a = 1
b = 2

try:
    print(1)
    assert a < b, "a isn't smaller than b"
    print(2)
    assert a > b, "a isn't bigger than b"
    print(3)

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

上記を見てもらうとわかるかと思いますが、基本的には、

assert 条件式, 条件式がFalseを帰す場合のエラーメッセージ

という形でassert文を定義します。上記の条件式がTrueとなれば「特に問題ない」ということなので何も起きませんが、これがFalseになるということは「問題あり」と判断されるので、その後のエラーメッセージとともに例外が発生します。

これを実行させてみます。

# python test.py
1
2
<type 'exceptions.AssertionError'>
a isn't bigger than b

2番目のassert文の条件式がFalseになるので、AssertionErrorが発生し、それをキャッチしていることがわかります。簡単ですね。

このAssertなのですが、ほかの例外クラスに比べて「テスト」の意味合いが強いです。そのため、Pythonを「最適化オプション」とともに起動すると、このasset文は無視されます。試しにオプション-OとともにPythonを呼び出してみます。

python -O test.py
1
2
3

今度はエラーが発生せずに、tryが最後までいってしまいましたね。このようにassertを使うと「テスト時にのみチェックを行う」ことが可能です。逆に言えばテストが本番環境では実施されないので、そのぶんプログラムが高速に動くことが期待されます。

このようにassert文はテストでのみ使われるものであるため、try/catchで補足せずに、エラーでプログラムを中断させてしまっても問題ありません。それで問題が起きるのであればテストに問題があるだけです。例外処理というよりも「バグ取りのための特別なもの」だとお考えください。

withとas

withとasは「コンテキスト」を扱うための特別な構文です。ほかの言語ではあまり見られない少々独特な概念なのですが、知っておいて損はないと思うので取り扱いたいと思います。

コンテキストという言葉は少しボヤッとしているのすが、「ある特定の処理」を実行するための状態(モード)だと言えるかもしれません。たとえばよくあるファイル処理なのですが、基本的には

  1. ファイルをオープンする
  2. 読み書き
  3. ファイルをクローズ

という流れがありますね。言ってしまえば、これは「ファイル処理のための状態(モード)に入っている」という状況です。当然ながらプログラムなので、オープンしっぱなしにして何もしないことも可能なのですが、一般的には上記のような「ある定型的な一連の流れを実行する」ことが多いです。

withとasを使った構文はこの一連の処理を実行するために存在しています。文法的にはtry/finallyをPython側が勝手に使ってくれているようなイメージで利用可能です。 その利用方法は、

with A as B
  処理

という形です。Aはコンテキストをサポートする特別なオブジェクトであり、Bはそれが代入されます。なお、as Bは省略可能ですが、コンテキストによってはその利用は実質的に必須かもしれません。

一番わかりやすい例であるファイル処理を通して、このwith/asの使い方を見てみます。以下のコードを見てください。

try:
    with open('hello.txt', 'r') as f:
        print(type(f))  # <type 'file'>
        for line in f:
            print(line),
except:
    print('error')

このプログラムはhello.txtを読み取り専用モードで開き、その中身を一行ずつプリントするというプログラムです。注目して欲しいのはそのなかでwith/asが使われていることです。ここではopen('hello.txt', 'r')がコンテキストをサポートするファイルオブジェクトを返し、それがfに格納されています。そしてこのfを使ってファイル処理をしています。

ただ、よくよく考えてください。これは以前に学んだファイル処理の以下のプログラムと似ています。

try:
    f = open('hello.txt', 'r')
    print(type(f))  # <type 'file'>
    for line in f:
        print(line),
    f.close()
except:
    print('error')

これもファイルをオープンしてfにファイルオブジェクトを格納して処理しています。ほとんど同じですが、with/asに加えてf.close()が追加されている点も異なります。

結論から言ってしまいますと、with/asを使うとファイルオブジェクトがクローズされることが保証されます。後者のコードだと、仮にfor文のプログラムの実行中に例外が発生した場合、そこで処理が中断されてexceptに飛んでしまうため、f.close()のコードが呼び出されません。一方、前者のwith/asはその構文を抜けたタイミングで自動的にファイルがクローズされるので、closeし忘れたり、プログラム的に飛ばされてしまったりするリスクを回避できます。

便利な機能なのでwith/asが使える場合は積極的に活用してみてください。なお、一応自分でwith構文に対応したコンテキストを持つオブジェクトの実装も可能ですが、初心者はそこまでやる必要はありません。


次回はマルチスレッド処理について扱います。

執筆者紹介

伊藤裕一(ITO Yuichi)

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

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

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

詳細(英語)はこちら