例外

数値は0で割ることはできないし、文字列が想定されている部分に数字が指定されればエラーになる。次のように不適切な記述をすれば、Pythonインタプリタからエラーが報告される。

>>> 10 * (1/0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero
>>> 4 + spam*3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'spam' is not defined
>>> '2' + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: must be str, not int
>>>

Pythonではこうしたエラーに対し、プログラムで対処することができる。比較的新しいプログラミング言語が例外処理を備えているように、Pythonにも例外処理の機能が備わっているためだ。例えば次のように「try-except制御構文」を記述すると、「10 * (1/0)」で発生したエラーが「except ZeroDivisionError:」以降で捕捉できていることがわかるだろう。これが例外処理の基本的な書き方だ。

>>> try:
...     10 * (1/0)
... except ZeroDivisionError:
...     print("except clause")
...
except clause
>>>

The Python Tutorial」には、例外処理の基本的な書き方として次のようなサンプルが掲載されている。

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...
Please enter a number: a
Oops!  That was no valid number.  Try again...
Please enter a number: b
Oops!  That was no valid number.  Try again...
Please enter a number: c
Oops!  That was no valid number.  Try again...
Please enter a number: あ
Oops!  That was no valid number.  Try again...
Please enter a number: い
Oops!  That was no valid number.  Try again...
Please enter a number: 10.19eadd
Oops!  That was no valid number.  Try again...
Please enter a number: 10.19
Oops!  That was no valid number.  Try again...
Please enter a number: 10
>>>

上記サンプルでは、標準入力から入力されたデータが整数以外の場合にエラーが発生するため、そのたびに「except ValueError:」へ処理が飛ぶようになっている。while文の中にあるため、「except ValueError:」の処理が終わると「while:」の初めに処理が戻る。こうして整数が入力されるまで同じ処理が繰り返され、整数が入力されるとbreakが実行されて処理が終了するという仕組みになっている。

ここで、例外が発生した場合には「break」まで処理が移動していないことに注目しておきたい。例外が発生すると即座に対応する「except」に処理が移っているわけである。

「except」に対応するエラーが存在しない場合、次のようにデフォルトの例外処理が行われ、処理が終了してしまう。

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except NameError:
...         print("Oops!  That was no valid number.  Try again...")
...
Please enter a number: a
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
ValueError: invalid literal for int() with base 10: 'a'
>>>

exceptには何も指定しないという書き方も可能だ。その場合、例外が発生すると次のようにexceptがワイルドカード的に全ての例外に一致するようになる。

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except:
...         print("Oops!  That was no valid number.  Try again...")
...
Please enter a number: a
Oops!  That was no valid number.  Try again...
Please enter a number: b
Oops!  That was no valid number.  Try again...
Please enter a number: c
Oops!  That was no valid number.  Try again...
Please enter a number: 10,12x
Oops!  That was no valid number.  Try again...
Please enter a number: 10.11
Oops!  That was no valid number.  Try again...
Please enter a number: 10
>>>

exceptは複数個書いておくことができるので、exceptだけの項目は最後に書くことになる。ここに想定外の例外が発生した場合の処理を記述しておくのである。

exceptの処理では、発生した例外そのものや、例外に指定された引数にアクセスしなければならないケースもある。その場合には、次のように「as」を使って例外にアクセスできるようにして使用すればよい。

>>> try:
...     raise Exception('spam', 'eggs')
... except Exception as inst:
...     print(type(inst))    # the exception instance
...     print(inst.args)     # arguments stored in .args
...     print(inst)          # __str__ allows args to be printed directly,
...                          # but may be overridden in exception subclasses
...     x, y = inst.args     # unpack args
...     print('x =', x)
...     print('y =', y)
...
<class 'Exception'>
('spam', 'eggs')
('spam', 'eggs')
x = spam
y = eggs
>>>

elseとfinally

Pythonの例外処理では「except:」に関して「else」と「finally」という指定もできるようになっている。elseは次のようにexceptの最後に加えて使用するもので、用意したexceptが全て一致しなかった場合に処理が実施される。だが、次の例のように、例外が発生しなかった場合にもelseが実行されるという点に注目していただきたい。この辺りがexceptに何も指定しなかった場合と処理が異なっている。

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...     else:
...         print("else clause")
...         break
...
Please enter a number: a
Oops!  That was no valid number.  Try again...
Please enter a number: b
Oops!  That was no valid number.  Try again...
Please enter a number: c
Oops!  That was no valid number.  Try again...
Please enter a number: 1
else clause
>>>

finallyはelseとは違い、何があっても必ず実行される項目だ。次のサンプルを見てもらえれば、例外が発生した場合でも、発生しなかった場合でも、breakが実行されても、finallyの内容が処理されてることがわかる。

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...         break
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...     finally:
...         print("finally clause")
...
Please enter a number: a
Oops!  That was no valid number.  Try again...
finally clause
Please enter a number: b
Oops!  That was no valid number.  Try again...
finally clause
Please enter a number: c
Oops!  That was no valid number.  Try again...
finally clause
Please enter a number: 1
finally clause
>>>

elseとfinallyは共存できる。例えば、次のような使い方だ。

>>> while True:
...     try:
...         x = int(input("Please enter a number: "))
...     except ValueError:
...         print("Oops!  That was no valid number.  Try again...")
...     else:
...         print("else clause")
...     finally:
...         print("finally clause")
...
Please enter a number: a
Oops!  That was no valid number.  Try again...
finally clause
Please enter a number: b
Oops!  That was no valid number.  Try again...
finally clause
Please enter a number: c
Oops!  That was no valid number.  Try again...
finally clause
Please enter a number: d
Oops!  That was no valid number.  Try again...
finally clause
Please enter a number:
Oops!  That was no valid number.  Try again...
finally clause
Please enter a number: 1
else clause
finally clause
Please enter a number:

finallyは主に「リソースを閉じる」といったような、エラーが発生してもしなくても処理すべき内容を書いておくケースで使われることが多い。

例外とクラス

exceptには例外ではなくクラスを指定することもできる。その場合、次のように動作する。一致したexceptが実行されていることがわかるだろう。

>>> class B(Exception):
...     pass
...
>>> class C(B):
...     pass
...
>>> class D(C):
...     pass
...
>>> for cls in [B, C, D]:
...     try:
...         raise cls()
...     except D:
...         print("D")
...     except C:
...         print("C")
...     except B:
...         print("B")
...
B
C
D
>>>

ここで、記述の順番を逆にしてExceptionから派生したBをCやDのexceptよりも前に記述すると、今度はBしか一致しなくなる。BがExceptionから派生したクラスであり、ここが先に一致するためだ。

>>> for cls in [B, C, D]:
...     try:
...         raise cls()
...     except B:
...         print("B")
...     except C:
...         print("C")
...     except D:
...         print("D")
...
B
B
B
>>>

例外は便利な機能だが、どの項目が一致するかはよく把握しておく必要がある。勘違いした状態でコーディングすると、問題が発生した場合の処理を適切に実施できなくなる。

例外処理はかならず書くべきか?

読者の中には、ツールとしてPythonを使いたいだけで、それほどエラー処理をしっかり書くつもりはないという方もおられるだろう。それはそれでよいと思うし、むしろそちらのほうが本連載の趣旨には合致する。

確かに、例外処理をきっちり書いておけば、さまざまなケースに対応できるコードに仕上げられる。ただし、処理がポンポン飛ぶことになるのでコーディングが難しくなるのは否めない。本連載では、あくまでもPythonは日々の業務を楽にするためのツールだととらえており、複雑な処理をコーディングするために使おうとはしていないので、他人の書いたコードが読める程度に機能を知っておいてもらえればと思う。

【参考資料】
The Python Tutorial