オブジェクト指向の解説まで終わりましたので、本連載で扱う内容はほぼ終わりです。今回からは今までの連載で話せなかった内容について取り扱います。

今回と次回は「例外処理」について扱います。まず例外処理がどのようなものなのか、想像がつかないかたもいらっしゃると思いますので、簡単に概要を説明したいと思います。その後、どのようにしてPythonで例外処理を実現するかを扱います。なお、例外はエラー、例外処理はエラーハンドリングと呼ばれることも多いです。

「例外」ってなに?

例外処理は名前を聞いてわかるように「例外(エラー)」に対する処理です。この例外は簡単に言ってしまえば、「期待されない動き」のことで、たとえば0での除算などがあげられます。算数や数学で学んだことがあるかもしれませんが、「0で何かを割る」というのは数学ではやってはいけないルールです。そのため、Pythonでもこの処理を実行しようとするとエラーになります。

試しにターミナルで実行してみましょうか。以下のようなエラー表示が確認できます。

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

エラーを読んでもらうとわかるように、0によるdivision(割り算)かmodulo(剰余)によりZeroDivisionErrorが発生していることがわかります。

このエラーが発生すると処理が中断されてしまいます。これも確認してみます。

def test():
    print(1)
    5 / 0
    print(2)

test()

上記のプログラムを実行してみます。

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

出力されるのは1だけであり、2は出力されていませんね。2が出力される前に「5を0で割る」という処理があり、そこでエラーが発生してprint(2)を実行する前に処理を打ち切ってしまっているためです。この「数字を0で割る」という処理はそもそもプログラムとして実行すべきではありません。上記のような直接的なバグコードを書くことはもちろん、たとえばどのような数字が入っているかわからない変数aを変数bで割る場合は、割る前にbの値が0でないかをチェックし、0の場合は割らないようにするなどの対処が必要です。

このようにある種のエラーは必ず発生しないようにすべきものだといえます。ただ、注意深くコードを書くことによりすべてのエラーが避けられるかというと、それは間違っています。たとえば「サーバーからデータを取ってくる」というネットワークを使ったプログラムを作成する場合、「サーバーに接続できるか」は自分が注意深くコードを書くかどうかというよりも、実行するマシンがネットワークにそもそも繋がっているか、つながっていてもサーバへのリーチャビリティはあるか、といったことなどに依存します。

このような処理をする場合は、例外処理を行うことが必須です。

例外処理のやりかた

例外に対する例外処理が必要なことはわかっていただけたと思います。ここではその例外処理をどのようにして実装するかという「概念」を学びたいと思います。

まず、例外処理はPython以前からある思想です。例外は言語に関係なく発生するので当然ですね。ただ、その例外処理の実装スタイルには大まかに分けて2つあります。ひとつめはC言語などで使われる「返り値チェック」を使うものです。そして2つめはJavaなどで使われる「try/catch」を使うものです。PythonはJavaと同じく後者を使うのですが、前者も知っておく必要があるので説明します。

Cのような「返り値チェック」の例外処理ですが、歴史的にはこちらのほうが古いです。その方式を簡単に言ってしまうと「ある関数を呼び出した時の返り値」が例外の値でないかをチェックするというものです。

以下の図を見てください。

「返り値チェック」の例外処理

ここでは関数Aがあり、この関数を呼び出しています。関数Aは「ある処理に失敗したら、返り値として-1を返す」という動きをすると決められています。自分でこのルールを作ることもあるでしょうし、すでに存在している関数のドキュメントにそう書かれていることもあります。この場合、この関数を使った際に返り値を取得し、その返り値が-1であるかどうかを確認します。仮にこの値が-1であれば関数Aは処理に失敗しているので、処理の中断なり別の回避策なりが必要です。上記例ではif文の中で例外処理をやっていますが、ここでgotoを使って例外処理を行う場所までジャンプしてしまうというのもよくある方法です。

関数の返り値のチェック以外にも、関数の引数に「ポインタと呼ばれている参照」を渡し、そこに関数内で特定の値をセットするという方法もあります。関数を呼び出した側でそのポインタの値をチェックすれば、引数と同じく成功したか失敗したかがわかります。このような形でポインタを使うところがC特有なので、比較的新しい高級言語しか使わない人には慣れない使い方かと思いますが、基本的には関数の返り値のチェックと大差はありません。これらがCなどで使われる代表的な例外処理方式です。

次にJavaで使われるtry/catchによる例外処理です。プログラミング言語の文法としてtry/catch方式の例外処理がサポートされている場合に利用可能で、Pythonもこれをサポートしています。自分でコードを書く場合はC方式の例外処理を実装することも可能ですが、ほかの人のコードを利用する場合はその例外処理をtry/catchで対処する必要があり、なおかつCのスタイルで例外処理を使うとコードも煩雑になるので、特に理由がないかぎりはtry/catchに沿った例外処理を使ったほうがよいと思います。

まぁ、ゴチャゴチャ言うよりも先に、どのように使うか見てみましょう。以下の図を見てください。

try/catchによる例外処理

try/catch方式は簡単です。例外が発生する可能性があり、その対処が必要な箇所をtryスコープで囲みます。このスコープ内でエラーが発生すると、その後ろにあるcatchスコープまでジャンプします。エラーが発生したあとのtryスコープ内のコードは実行されないので注意してください。上記図で言うと、処理2の前にエラーが発生すると、処理2は実行されません。

また、tryスコープの中でエラーが発生しなかった場合はcatchのスコープ内の処理は実行されません。tryの中でエラーが起きようと起きまいと、try/catchの後にあるコードは実行されます。もしエラーが発生した場合に処理を打ち切りたいのであれば、catchの中でプログラムを終了させるなり、returnで関数を抜けてしまうなりする必要があります。

try/catchについては実際にPythonで使い方を学ぶと理解できると思いますので、詳しい話をするのはここでは控えて、使いながら学んでいきたいと思います。

Pythonのtry/catchを使ってみる

Pythonのtry/catchは簡単です。まず一番シンプルな使い方は以下のようなものになります。

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/catchのcatchはPythonではexceptになっていますが、それ以外は先程説明したものとまったく同じです。tryの中でエラーが発生すれば、それ以後のtry内の処理を打ち切ってexceptにジャンプします。今回はわざと5/0でエラーを発生させています。

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

# 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のエラー発生後のコードであるprint('3: inside of try scope') は実行されずに、exceptにジャンプしてその中の処理を実施しています。試しにこの5 / 0をコメントアウトして実行してみます。

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')

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

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

先ほどと異なり、エラーが発生していないのでprint('3: inside of try scope')が実行されています。またエラーが発生しなかったので、except内の処理は呼びだされず'4: inside of except(catch) scope'の表示がなくなっていることもわかります。そして、try/catchの外にある'1: outside of try/catch'と'5: outside of try/catch'は常に実行されています。

これがPythonの簡単な一番簡単な例外処理の方式です。Pythonではより細かく詳細な例外処理の実装を行うことも可能ですが、私は正直なところそれほど複雑なものは利用しません。なぜなら大規模なコードを書くというよりも小さいスクリプトを書くためにPythonを使っているためです。

これ以後のテクニックは必須ではないのですが、知っておくとなにかと便利なので興味がある人は引き続き読んでください。また、Javaなどではこのあたりのテクニックはかなり使うので、最終的にほかの言語を使う予定がある人は読んだほうがいいかもしれません。

例外Exceptionクラスとその子クラス

前回までにお話した継承を思い出してください。継承では、親クラスに大まかな実装を行い、子クラスにより詳細な実装をするのでした。たとえば親クラスが「車クラス」だとすると、子クラスは「乗用車クラス」「トラッククラス」「スポーツカークラス」といった感じになります。

この継承は例外処理にも関わってきます。先ほど深い説明なしにexceptを以下のように使いました。

except Exception:
    print('4: inside of except(catch) scope')

exceptの後にあるExceptionは、実は例外処理のためのクラスです。このExceptionは継承されたクラスがさまざまあり、たとえばIO(入出力)のエラーを扱うためのIOErrorなどがあります。これはちょうどExceptionが先ほどの説明の車クラスにあたり、IOErrorが乗用車クラスにあたります。

このエラークラスとその使い方について覚えておいてもらいたいことは3つあります。 ひとつめは発生したエラーの種類に応じて呼び出されるエラーのクラスが異なるということです。たとえば、上記のIOError は当然ながらIO系の処理が失敗した際に利用されますが、まったく関係ないエラーである0による除算では利用されません。

ふたつめはexceptに親クラスを指定した場合は子クラスのエラーも対応できるということです。たとえばIOErrorの例外は親クラスであるExceptionでも対応可能です。

そして、最後はこのエラーを利用するexceptは複数書くことができるという点です。複数書いた場合は先頭から順にチェックしていき、最初にマッチした処理が実行されます。どのexceptもマッチしなければ例外処理が実行できずにエラーで停止していまいます。

さっそくコードを書きながら確かめてみましょう。まず、以下のコードがあります。exceptが2つあり、それぞれIOErrorとExceptionと記載されていますね。2つだけでなく、好きなだけexceptを書くことができます。

try:
    f = open('helloworld.txt', 'r')

except IOError:
    print('io error')

except Exception:
    print('exception')

今回は存在しないファイルhelloworld.txtを読み込もうとしてエラーを発生させます。これはIOErrorが発生します。さっそく実行してみましょう。

# python test.py
io error

表示された'io error'を見てわかるように、1番目のexceptが呼び出されていますね。 'exception'という表示がないことから、2番目のexceptは呼び出されていないことがわかります。これは「最初にマッチした処理が実行」されるという仕組みがあるからです。

次に発生させるエラーを0除算に変えてみます。

try:
    5 / 0

except IOError:
    print('io error')

except Exception:
    print('exception')

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

# python test.py
exception

先ほどと異なり、2番目のexceptが呼び出されています。これは1番目のexceptが、発生したエラーにマッチしておらず、無視されたためです。今回はIOErrorが1番目に指定されていますが、5/0で発生したエラーはIOErrorではなくZeroDivisionErrorなので、マッチしません。ただ、2番目のExceptionはZeroDivisionErrorの親クラスなのでマッチし、2番目のexceptが呼び出されています。

また、先ほど言ったように、どのexceptもマッチしないとエラーになります。試しに2番目のexceptを削ってみます。

try:
    5 / 0

except IOError:
    print('io error')

print(1)

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

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

最後の'print(1)'に対応する出力がないことから、グローバルレベルでプログラムの処理が打ち切られていることがわかりますね。発生するエラーの種類によってさまざまな例外処理を切り替える必要がある場合は、このように複数のexceptを使って例外処理を実装すると簡単です。

なお、先ほど言ったように前のexceptにマッチしたら、後のexceptはチェックされません。そのため、以下のコードで2番目のexcept IOErrorが呼び出されることはありません。

except Exception:
    print('exception')

except IOError:
    print('io error')

前のexceptになんにでもマッチするものを書いてしまうと、例外はすべてそこで処理されてしまいます。つまり、前のexceptほど詳細なものを書き、後半ほど大きな範囲をカバーできるクラスを書く必要があるということです。

どのような例外処理があるかは以下のドキュメントを参照ください。


今回は例外処理の基本について学びました。来週はこの続きを扱います。

執筆者紹介

伊藤裕一(ITO Yuichi)

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

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

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

詳細(英語)はこちら