Python♪例外の基本(elseの必要性、raiseの使い方、ユーザー定義例外)

「try文の実行順序が分かりにくい」「elseって必要なのかな」「raiseの使い方がわからない」「クラスの引数のExceptionってなに」「exceptのあとのasってなに」「ユーザー定義例外?」例外処理のコードって、いつも使うわけではないから、いざ使おうと思うと、ごちゃごちゃになってしまいませんか。

 そこで、例外を初めて勉強する人や、忘れて思い出したい人のために、例外の基本を整理したいとおもいます。

0.チュートリアル学習のポイントシリーズ

この記事は「Pythonチュートリアルのポイント整理シリーズ」の記事です。一連の記事は、以下のリンク集を参照してください。
「Pythonチュートリアル」のポイント整理シリーズ

1. 構文エラーと例外

 「エラー」にはPythonの構文的に許されていない記述をしたときに発生する「構文エラー」と、構文的には正しいですが、プログラムを続行できない「例外」があります。
 一般的には、「エラー」と「例外」の関係は明確な定義がないようですが、Pythonのチュートリアルでは、以下のように明記されています。ただ、他のプログラミング言語では、同じ定義ではないかもしれません。
「エラーには (少なくとも) 二つのはっきり異なる種類があります。それは 構文エラー (syntax error) と 例外 (exception) です。」 

(1) 構文エラー

「構文エラー」は構文的に許されていない記述をしたときに発生するエラーです。コードを修正する必要があります。なお、以下の(a)構文エラーの種類と(b)構文エラーの例は、ざっと読み飛ばしましょう。

(a)構文エラーの種類

SyntaxError, IndentationError, TabErrorなど

(b) 構文エラーの例

print(‘Hello World) #SyntaxError
print ‘Hello World’ #SyntaxError

print(‘Hello’)
    print(‘World’) #IndentationError

(2)例外

「例外」は構文的には正しいですが、プログラムを続行できないエラーです。

(a) 例外の種類

ZeroDivisionError, TypeError, IndexError, NameError, FileNotFoundError, AttributeError, KeyError, ImportError, UnicodeDecodeError, UnicodeEncodeErrorなど

(b) 例外の例

x = 1 / 0 #ZeroDivisionError
x = 1 + ‘2’ #TypeError

(3) Exceptionクラス

 Pythonのチュートリアルでは、「クラス」のページは「エラーと例外」のあとですので、クラスの勉強はまだの人もいると思います。クラスというのは、簡単に言うと「def関数」をもっと便利にしたようなものです。そして、構文エラーの種類であるSyntaxErrorや、例外の種類であるZeroDivisionErrorなどは、ただのエラーの名称ではなくクラスなのです。それぞれのエラーについては、必要に応じて覚えればよいですが、例外の勉強をするためにはExceptionクラスを最初に覚えましょう。
 なお、クラスは「元となるクラス」の機能を継承(拡張)して、新しいクラスを作ることができます。そして、ZeroDivisionErrorのようなエラーに関するクラスは全てExceptionクラスを継承(拡張)してできたクラスなのです。
 細かい内容は理解できなくても、Exceptionクラスが、他の例外のクラスの元になるような存在であることを知っておく必要があります。

2. 例外処理

 例外は、致命的なエラーではありません。想定できる例外であれば、プログラムの実行を中止せずに続行することもできます。
 例えば、ファイルを読み込もうとして、そのファイルが存在しなかった場合、プログラムが終了してしまうよりも、入力ファイル指定のやり直しができた方が便利です。他にも、行列計算でゼロで除算してしまう例外が発生したときに、考えられる対応を表示してくれれば、すぐに入力データを修正して対応することができるかもしれません。

 このように、例外が生じた場合でもプログラムを終了せず、その時の対応を指定することができれば便利です。そして、この例外に対する処理のことを「例外処理」といいます。

3. try文で覚えるべき基本的な節

 例外処理にはtry文を使います。なお、try文は様々な節に分かれており、必ず必要なのが、try節とexcept節です。例えばコード01では、try文がtry節とexcept節に分かれています。

 以下、それぞれの節の書式と機能の概要を説明します。

(1) try:

 例外が想定されるコードをtry節に記述します。そして、try節で例外が発生すると、その行以降のtryのコードの実行は中断し、except節など、他の節に実行が移ります。
 つまり、例外処理をしなければ、例外が発生すると無条件でプログラムが終了しますが、try文を用いることにより、プログラムを終了せず、例外が発生したときの手順に移ることができます。

(2) except 例外の種類:

 exceptの後に例外の種類を指定します。そして、指定した例外が発生すると、except節に記述されたコードが実行されます。例外の種類ごとに複数のexcept節を設けることができます。
 なお、except節は省略することできません。1つ以上のexcept節が必要です。また、try節で発生した例外が、複数のexcept節に該当するときには、一番最初のexcept節のみが実行されます。

(3) except 例外の種類 as 変数:

 except節で、asの後に「変数」を指定すると、この変数にエラーの情報を渡します。詳細は後述します。

(4) except:

 except節で例外の種類を指定しない場合、どんな例外が発生しても、このexcept節に記述されたコードが実行されます。また、例外クラスの元となるExceptionクラスを指定して、「except Exception:」としても同様の結果になります。これらの書き方は、想定外の例外を許容したまま、プログラムの実行が継続される恐れがあるため推奨されていません
 なお、例外の種類を指定しないexcept節は、例外の種類を指定するexcept節の後に置く必要があります。

(5) else:

 try節で全く例外が送出されなかったときに実行されます。
 なお、except節よりも後ろに置く必要があります。

(6) finally:

 例外が発生したかどうかに関わらず、 try 文を抜ける前に常にfinally節のコードが実行されます。なお、try文で想定外の例外が生じた場合には、例外処理を行わなかったときと同様に例外が送出され、実行が停止しますが、finally節のコードはその前に実行されます。
 なお、finally節は、except節、else節よりも後ろに置く必要があります。

4. try文の実行順序

 ここでは、図を使って、try文の流れを説明します。図のコードの左側の赤い矢印が実行順序を示します。

(1)try節とexcept節だけの簡単な例題

 まずは、try節とexcept節だけの簡単な例題を見てみましょう。

(a) try節で例外が生じなかった場合

 下図は、上で紹介したコード01です。try文の中に、try節とexcept節があります。try節で例外が発生しなかった場合は、except節は実行されません。

(b) try節で例外発生→except節で指定する例外と一致

 try節の中で例外が発生しました。そこで、例外が発生した行でコードの実行を中断し、次の節に実行を移します。Try節で発生したエラーが、except節で指定したTypeErrorと一致するので、except文が実行されます。想定内の例外をexcept文で適切に処理したので、プログラムは終了せず、最後の行のprint(‘プログラムの終了’)が実行されました。

(c) try節で例外発生→except節で指定する例外と一致しない

 try節で例外が発生し、try節のコードの実行を中断。その後、次の節に実行が移るが、この例外はexcept節で指定される例外ではありません。想定外の例外なので、プログラムは例外を送出し、実行を停止します。したがって、最後の行のprint(‘プログラムの終了’)は実行されません。

(2) else節、finally節を追加した例題

 次に、try節、except節だけではなく、 else節、finally節を追加した例題を見てみましょう。

(a) try節で例外が生じなかった場合

 try節で例外が発生しなかったため、try節のコードはすべて実行されます。
 except節は例外が生じなかったので、実行されません。
 else節は、例外が生じなかった場合に実行される節なので、実行します。
 finally節は、必ず実行する節なので、実行します。
 例外が生じなかったため、最後の行のprint文は実行されます。

(b) try節で例外発生→except節で指定する例外と一致

 try節で例外が発生したため、try節のコードは中断し、次の節に移ります。
 except節で指定した例外だったため、except節を実行します。
 else節は、例外が生じなかった場合に実行される節なので、実行しません。
 finally節は、必ず実行する節なので、実行します。
 想定内の例外だったのでプログラムは停止せず、最後の行のprint文は実行されます。

(c) try節で例外発生→except節で指定する例外と一致しない

 try節で例外が発生したため、try節のコードは中断し、次の節に移ります。
 except節で指定した例外とは異なるため、except節は実行しません。
 else節は、例外が生じなかった場合に実行される節なので、実行しません。
 finally節は、必ず実行する節なので、実行します。
 想定外の例外だったので、プログラムは例外を送出し、実行を停止します。

 いかがでしょうか、try文の各節の働きや流れが整理できたのではないでしょうか。

5. else節の必要性

 この節ではelse節の必要性について説明します。

(1) else節のコードは、try節に書いても同じ?

 よくよく考えると、else節は例外が生じなかったときに実行されるのであれば、コードをelse節に書いても、try節に書いても実行の順序は同じはずです。else節って本当に必要なのでしょうか?

(a) try節で例外発生行の後に記述されたコード
 →例外が生じなかったときに実行。

(b) else節に記述されたコード
 →例外が生じなかったときに実行。

(2) else節はこんなときに必要です

 それでは、コード07とコード08を比べてみましょう。コード07は、5行目の「x1 = 1 / y」で例外が生じると想定されるコードだとします。一方、6行目の「x2 = 2 / z」は、コードを作ったときには例外が生じるとは思っていなかったとします。
 例外とは、そもそも、思ってもいなかった例外が生じたときには、プログラムを停止すべきものです。しかし、コード07では、6行目で想定外の例外が生じても、except節で適切に処理がなされ、プログラムは続行されてしまいます。想定外の例外が原因で更に大きなエラーを生じてしまう可能性があります。

#コード07
y = 1
z = 0
try:
    x1 = 1 / y #例外が生じる可能性があると考えているコード
    x2 = 2 / z #想定外の例外
except ZeroDivisionError:
    print('分母がゼロ')
print('プログラムの終了')

 一方、コード08のようにelse節に「x2 = 2 / z」を記述すると明快です。例外を想定していない「x2 = 2 / z」で例外が発生すると、プログラムは例外を送出して停止します。

#コード08
y = 1
z = 0
try:
    x1 = 1 / y #例外が生じる可能性があると考えているコード
except ZeroDivisionError:
    print('分母がゼロ')
else:
    x2 = 2 / z #想定外の例外
print('プログラムの終了')

 このように、else節を使用することで、例外処理の対象を明確にし、思わぬエラーが生じたときの対応を楽にすることができます。

6. 例外処理で例外の情報を出力

 例外処理を行うときに例外の情報を出力するには、いくつかの方法がありますので紹介します。

(1) x = 10 / 0 の実行

 それではまず、例外処理を行わなかったときの例外の送出について見てみましょう。コード09のように「x = 10 / 0」を実行すると、出力09のような例外が送出され、実行が停止します。出力の最後の行ではエラーの種類である「ZeroDivisionError:」のあとに、「division by zero」という例外の説明が出力されています。

#コード09
x = 10 / 0
#出力09
Traceback (most recent call last):
   ・
   ・
   ・
    x = 10 / 0

ZeroDivisionError: division by zero

 それでは、例外処理を行ったときにも、この例外の説明を出力することはできないのでしょうか。

(2) except [例外の種類] as [変数名]:

 except節で、asの後に[変数]を指定すると、この変数にエラーの情報を渡します。コード10の5行目でeeeのtypeを出力すると、eeeは、クラスZeroDivisionErrorのインスタンスであることがわかります。また、eee.argsは例外の説明を要素にもつタプルであることがわかります。

#コード10
try:
    x = 10 / 0
except ZeroDivisionError as eee:
    print(type(eee))
    print(eee.args)
    print(eee.args[0])
    print(eee)
#出力10
<class 'ZeroDivisionError'>
('division by zero',)
division by zero
division by zero

 そして、コード11のように記述することで、コード09と同様の出力をすることができます。

#コード11
try:
    x = 10 / 0
except ZeroDivisionError as eee:
    print('ZeroDivisionError:',eee)
#出力11
ZeroDivisionError: division by zero

 asを用いた変数への情報渡しは、例外の種類を指定しないタイプのexcept節「except:」では使用できません。そこで同様のことがしたい場合には、コード11bのように例外の種類の指定をExceptionにします。Exceptionクラスは、全ての例外クラスの元になるクラスですので、例外の種類を指定しない「except:」と同様に、全ての例外に対してexcept節のコードを実行させることができます。

#コード11b
try:
    x = 10 / 0
except Exception as eee:
    print('ZeroDivisionError:',eee)
#出力11b
ZeroDivisionError: division by zero

(3) sys.exc_info()

 pythonの標準ライブラリーのsysモジュールのsys.exc_info()関数を用いることで、同様のことができます。

#コード12
import sys
try:
    x = 10 / 0
except ZeroDivisionError:
    print(sys.exc_info())
    print(sys.exc_info()[0])
    print(sys.exc_info()[1])
    print(sys.exc_info()[2])
#出力12
(<class 'ZeroDivisionError'>, ZeroDivisionError('division by zero',), <traceback object at 0x0000025E937D9FC8>)
<class 'ZeroDivisionError'>
division by zero
<traceback object at 0x0000025E937D9FC8>

 コード13により、コード11と同様の出力をすることができます。

#コード13
import sys
try:
    x = 10 / 0
except ZeroDivisionError:
    print('ZeroDivisionError:',sys.exc_info()[1])
#出力13
ZeroDivisionError: division by zero

 sys.exc_info()を用いる場合には、except節で例外の種類を指定しない場合でも使用できます。出力は出力13と同じになります。

#コード13b
import sys
try:
    x = 10 / 0
except:
    print('ZeroDivisionError:',sys.exc_info()[1])
#出力13b
ZeroDivisionError: division by zero

7. 例外を送出するraise

 例外を意図的に発生させたいときに使うのがraise(レイズ)です。raiseでは、以下の(1)~(3)の3つの書式を覚えましょう。

(1) 書式1: raise 例外の種類

 raiseの後に「例外の種類(例外クラス)」を指定することにより、その例外を意図的に発生させることができます。コード14は、「x  = 10 / 0」とすることで、ZeroDivisionErrorを発生させていますが、コード15では、raise ZeroDivisionErrorによりZeroDivisionErrorを発生させています。

#コード14
try:    
    x = 10 / 0
except ZeroDivisionError as eee:
    print('eee.args:', eee.args)
    print('eee:', eee)
#出力14
eee.args: ('division by zero',)
eee: division by zero

 コード14とコード15の違いは、asを用いて変数eeeにエラーの情報を渡しても。出力15では、情報がなにも出力されません。

#コード15
try:    
    raise ZeroDivisionError 
except ZeroDivisionError as eee:
    print('eee.args:', eee.args)
    print('eee:', eee)
#出力15
eee.args: ()
eee:

(2) 書式2: raise 例外の種類(’例外の情報’)

 raiseで発生させたい「例外の種類(例外クラス)」の後の()のなかに「例外の情報」を文字列型で記入します。

#コード16
try:    
    raise ZeroDivisionError('ゼロによる除算') 
except ZeroDivisionError as eee:
    print('eee.args', eee.args)
    print('eee:', eee)
#出力16
eee.args ('ゼロによる除算',)
eee: ゼロによる除算

 また、2つ以上の「例外の情報」を記入することができ、その場合はコード17の出力は出力17のようになります。

#コード17
import sys
try:    
    raise ZeroDivisionError('ゼロによる除算','2つめの情報') 
except ZeroDivisionError as eee:
    print('eee:',eee)
    print('eee.args',eee.args)
    print('eee.args[0]:',eee.args[0])
    print('eee.args[1]:',eee.args[1])
    print(sys.exc_info())
#出力17
eee: ('ゼロによる除算', '2つめの情報')
eee.args ('ゼロによる除算', '2つめの情報')
eee.args[0]: ゼロによる除算
eee.args[1]: 2つめの情報
(<class 'ZeroDivisionError'>, ZeroDivisionError('ゼロによる除算', '2つめの情報'), <traceback object at 0x00000230F66F5388>)

(3) 書式3: ecsept節のraise

 except節の中で引数のないraiseを用いると、try文の中で発生した例外が再送出されます。コード18では3行目でZeroDivisionErrorが送出されますが、try節の中なので例外の内容は出力されず、プログラムも停止しません。次にtry節からexcept節に実行が移りますが、6行目のraiseにより、ZeroDivisionErrorが再送出され、出力18のような出力がなされた後、プログラムは実行を停止します。出力は「ZeroDivisionError:ゼロによる除算」となっています。

#コード18
try:    
    raise ZeroDivisionError('ゼロによる除算') 
except ZeroDivisionError as eee:
    print('eee:',eee)
    raise
#出力18
eee: ゼロによる除算
Traceback (most recent call last):
   ・
   ・
   ・
    raise ZeroDivisionError('ゼロによる除算')

ZeroDivisionError: ゼロによる除算

 コード19では6行目のraiseはtry文の外にあります。この場合、6行目のraiseで、「アクティブな例外がない」としてRuntimeErrorが発生します。なお、raiseはfinally節に記述しても同様にRuntimeErrorが発生しますので、raiseは、基本的にexcept節に記述すると考えてください。

#コード19
try:    
    raise ZeroDivisionError('ゼロによる除算') 
except ZeroDivisionError as eee:
    print('eee:',eee)
raise
#出力19
eee: ゼロによる除算
Traceback (most recent call last):
   ・
   ・
   ・
    raise

RuntimeError: No active exception to reraise

(4) raiseを使った例

それでは、どんな使い方をするのでしょうか。raiseを使った例を紹介します。x + x / aの結果を返す関数xxx()において、a = 0の時にはもちろん例外が発生しますが、aの絶対値が0.000000000000001よりも小さいときにも、例外が発生するようにしてみました。x = 1に対して、a = 0, a = 0.000000000000000001, a = 0.1とした場合の実行結果が出力20aです。

#コード20
def xxx(x, a):
    try:
        y = x + x / a
        if -0.000000000000001 < a and a < 0.000000000000001 and a != 0:
            raise ZeroDivisionError('aの絶対値 < 0.000000000000001')
    except ZeroDivisionError as eee:
        print('warning:関数xxx()')
        raise
    else:
        return y

#以下、関数の実行
try:
    y = xxx(1, 0)
except ZeroDivisionError as eee:
    print(eee.args[0])
else:
    print('x + x / a =',y)

try:
    y = xxx(1, 0.000000000000000001)
except ZeroDivisionError as eee:
    print(eee.args[0])
else:
    print('x + x / a =',y)

try:
    y = xxx(1, 0.1)
except ZeroDivisionError as eee:
    print(eee.args[0])
else:
    print('x + x / a =',y)
#出力20a
warning:関数xxx()
division by zero
warning:関数xxx()
aの絶対値 < 0.000000000000001
x + x / a = 11.0

例外の具体的な対応を関数xxx()の中ではなく、関数の外側で行いたい場合、9行目のraiseを削除すると、関数の外に例外を送出しないため、関数の外で例外処理を行うことができなくなります。

したがって、コード20の9行目のraiseを削除すると、出力は以下(出力20b)のようになってしまいます。

#出力20b
warning:関数xxx()
x + x / a = None
warning:関数xxx()
x + x / a = None
x + x / a = 11.0

8. ユーザー定義例外

 ユーザー定義の例外を作成するには、Exceptionクラスを利用します。ここまでにExceptionクラスが、他の例外のクラスの元になるような存在であることを説明しましたが、ユーザー定義例外はExceptionクラスを継承(拡張)することによって作成します。クラスの継承は、コード21のように、「class クラス名(元になるクラス)」という書式で行います。
 2行目で定義されたクラスA_errorは「Exceptionクラス」を継承したユーザー定義の例外クラスです。3行目のpassは、何もしないことを明示する命令です。classを宣言する2行目の命令文のあとに何も書かないとエラーになるので、体裁を整えるためだけに記述されています。同様にクラスB_errorはA_errorを継承したユーザー定義の例外クラスです。
 8行目以降は、ユーザー定義の例外クラスをtry文で使用した例です。ここで、注目すべき点は、Exceptionクラスや、A_errorクラスを継承したB_errorクラスは、except節で指定する例外の種類がB_errorのときだけではなく、A_errorのときも、 Exceptionのときも実行される点です。発生した例外のクラスの名称だけではなく、元になる親のクラスの名称でもexcept節が実行されるのです。
 なお、例外の情報を出力したい場合には、24行目のように()の中に、例外の情報を記述します。

#コード21
class A_error(Exception):
    pass

class B_error(A_error):
    pass

try:
    raise B_error
except B_error:
    print('例外B_errorが発生しました。')

try:
    raise B_error
except A_error:
    print('例外A_errorが発生しました。')

try:
    raise B_error
except Exception:
    print('例外Exceptionが発生しました。')

try:
    raise B_error('例外の情報1','例外の情報2')
except B_error as eee:
    print(eee)
    print(eee.args)
    print(eee.args[0])
    print(eee.args[1])
#出力21
例外B_errorが発生しました。
例外A_errorが発生しました。
例外Exceptionが発生しました。
('例外の情報1', '例外の情報2')
('例外の情報1', '例外の情報2')
例外の情報1
例外の情報2

 コード22は、8行目のraiseで例外を再送出し、プログラムを停止させた場合の例です。「x = 10 / 0」などでおなじみの出力「ZeroDivisionError: division by zero」と出力22の「A_error: 例外の情報」を比べてみてください。「ZeroDivisionError:」 に相当する部分がユーザー定義例外の「A_error: 」となっており、「division by zero」に相当する部分が「例外の情報」になっています。

#コード22
class A_error(Exception):
    pass

try:
    raise A_error('例外の情報')
except A_error as eee:
    raise
#出力22
Traceback (most recent call last):
   ・
   ・
   ・
    raise A_error('例外の情報')

A_error: 例外の情報

 以上が、例外の基本的な説明です。そんなに難しくはないと思いますが、順を追って整理しないと、とっつきにくい内容です。また、クラスを勉強する前か後かによっても、理解のしやすさが異なると思います。
 クラスについては、まずは以下の①~③の知識だけ記憶しておき、詳しいことはあとで勉強すればよいと思います。
①クラスとは関数を少し複雑にしたようなものである。
②Exceptionクラスは、他の例外のクラスの元になるような存在である。
③「class クラス名(元になるクラス)」という書式(コード20を参照)で、元になるクラスを継承したユーザー定義の例外クラスが作成できること。

私が実際に購入した教材のご紹介

以下、私が実際に購入したPythonの教材をまとめてみました。 Pythonを学習する上で、少しでもお役に立つことができればうれしいです。

Python♪私が購入したPythonの書籍のレビュー

UdemyのPythonの動画講座を書籍を買う感覚で購入してみた