Pythonの例外処理はコードの可読性を上げる便利な機能

Pythonコーディングは例外を書こう!(スローしよう)

システムがクラッシュした結果が例外だというイメージを持つために、なるべくif文を書いて例外処理を書こうとしないことがある。

でも、それは間違っている。

例外はよく起こることで、例外と親しくして例外を受け入れる、積極的にスローすることが大切だ。

例外が起きたらシステムは終わりだ!なんて考える必要はなく、例外を当然のこととして処理しよう。

例外を起こす(raise)することはとてもPythonという言語の理にかなっていて、例外処理を書くのを避けようとすることこそ避けるべきです。

Pythonの例外はプログラムを読みやすくする

よく起こる例外一覧

例外の中でも主に押さえておきたいのは、

TypeError ...型エラー
IndexError ...インデックスエラー(たとえばリストの中身が3個しかないのに4個目にアクセスしようとした)
NameError ...ローカルまたはグローバルの名前が見つからなかった時。定義がない、スペルミス
ValueError ...値が適正でない(整数じゃなきゃいけないのに文字列を入力、など)

です。

すべてをif,elif,elseに頼らない

さっそく例外を見てみましょう。

>>> a = 1
>>> b = 0
>>> x = a / b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

ゼロで割ることはできないので、Pythonは「ZeroDivisionError」という例外を吐いてくれます。もしゼロで割ることができる世界で処理しなければならない問題があるなら、このZeroDivisionErrorをキャッチした際に処理を書くことになります。「ゼロで割る」ということに関する深入りは当記事では避けます。

では、例外を恐れて、例外をスローしたくなくてif文を書くとこうなります。

a = 1
b = 0

if b != 0:
    x = a / b
    print(x)
else:
    print('ゼロでは割れへんで')

>>> ゼロでは割れへんで

ゼロで割りたくないんだから、if文でゼロでないか確かめてから割ればええやん!っていう考えです。

でも、ちょっと待って下さい。

そのif文の処理ってプログラムの本質的なところではない、言い換えれば本当にやりたいことじゃないのにのさばっているような感覚を受けませんか?

上の例の場合、本当にやりたいことは

a / b

の結果を見たいわけです。焦点はa/bという処理にあるんですね。
こういうときには、「本当に俺がやりたいんはa/bっていうプログラムやねん!」と主張して、まさに例外が発生したときにはその処理をしようと考えれば良いわけです。tryexceptを利用します。

”例外”のケースをきちんと分けて、プロセスをわかりやすく

a = 1
b = 0

try:
    x = a / b
    print(x)
except ZeroDivisionError:
    print('ゼロで割ってしまったね〜')

>>> ゼロで割ってしまったね〜

たとえば上記のような割り算をするプログラムを考える際には、前提知識として「ゼロで割ってはいけない」ということを知っている必要があります。
そして知っているからこそ、ゼロで割らないようにするプログラムの書き方を考えますよね。だってゼロで割ったら例外が起きてプログラム止まっちゃうし。

つまり、コーディングしようという段階でどんなエラーが起きるか、例外が起きるのか考えています。
これがとても大事です。自分の書くプログラムがどういう時に上手く処理できないのか想定するわけです。

tryとexceptを利用して制御フローをコントロールしましょう。

if文とは条件分岐。例外処理ではない。

では逆に、if文を活躍させたい時はどんな時なのでしょうか?
if文は例外を予防するためのものではなく、プログラムの条件分岐です。

s = input("整数を入力してね:")
try:
    i = int(s)
    if i < 10:
        print('10より小さい数字やな')
    else:
        print('10以上の数字やな')
except ValueError:
    print('整数を入力してって言うたやん...')

input()を使って入力を受け取ってます。
「整数を入力してね」と書いており、整数が入力されることを想定しています。それが当たり前だという論理で、それ以外のものが入力されてくるのは”例外”だということです。そして、入力されてきた値が10より小さいのか、10以上なのかをif文で判定しています。このプログラムで本当に行いたいのは入力されてきた整数を10と比較することです。

言いたいのは、本質的ロジックはif文で書くということです。そしてロジックとは別の部分、すなわち入力がaaaみたいなのかもしれないし、5.4のように小数が入力されるかもしれないようなことっていうのは例外を使って書きます。

例外を複数キャッチする

例外はプログラムによっては2種類以上起きる可能性があります。
たとえば、

except(ValueError, ZeroDivisionError):

のようにタプルで複数の例外を記述できます。
では、2つの例外が起きる可能性を見てみましょう。

s = input("整数を入力してね:")
try:
    i = int(s)
    if i < 10:
        x = i / 10
        print(f'答えは{x}')
    else:
        x = i / 0
        print('例外発生したのでこのprintは実行されません')
except (ValueError, ZeroDivisionError):
    print('値が不正かゼロで割られたためエラーが発生しました')

raise文で指定された例外を起こす

raise文を使えば例外を起こさせることができます。

極端な例ですが、あるプログラムの例外処理を書き、exceptの内容が想定していた例外と異なる場合にraiseで例外をつくれます。

s = input("整数を入力してね:")
try:
    i = int(s)
    if i < 10:
        print('10より小さい数字やな')
    else:
        print('10以上の数字やな')
except ZeroDivisionError:
    print('これは実行されませんね')
except:
    raise ValueError('raiseにより例外発生')

単にexcept:とだけ書くと、すべての例外を拾ってくれます。しかし、ValueErrorは自分で書いただけなので、本当にValueErrorなのかどうかわかりません。なので例外の原因が特定しずらいexcept:は推奨されていませんどんな例外が起きる可能性があるのか明確に書くべきだということですね。

例外はカスタムクラスを自作できる

ビルトインクラスであるExceptionのサブクラスとして、新しい例外を定義することができます。
書き方としては、

class exceptionName(baseException): pass

実際にExceptionのサブクラスをつくって自作例外を発生させてみましょう。

class MyException(Exception):
    pass


s = input("整数を入力してね:")
try:
    i = int(s)
    if i < 10:
        x = i / 10
        print(f'答えは{x}')
        if x < 0.5:
            print('OKOK')
        else:
            raise MyException()
    else:
        x = i / 0
        print('例外発生したのでこのprintは実行されません')
except (ValueError, ZeroDivisionError):
    print('値が不正かゼロで割られたためエラーが発生しました')
except MyException:
    print('自作の例外が発生')

上記のようなコードで「整数を入力してね:」のあとに5,6,7,8,9のいずれかを入力すると、
自作の例外を発生させることができますね。