プログラムの実行時に想定外の事象が発生すると、エラーになってプログラムが停止します。
このエラーのことを「例外」と表現しますが、この例外が発生した場合、それを検知して何らかの処理をさせる仕組みが、多くの言語には備わっています。
ただ、言語によって振る舞い方が変わってきますので、今回はPythonをテーマに例外処理の仕方について解説したいと思います。
Pythonでの例外の処理方法
Pythonでは try ~ except ~ else ~ finally というステートメントを使います。
構文 | 記述する内容 | 備考 |
---|---|---|
try | 例外が発生する可能性のある処理を記述 | 必須 |
except | 例外が発生した時に実行したい処理を記述 | 必須 |
else | 例外が発生しなかった場合に実行したい処理を記述 | 省略可能 |
finally | 例外の発生に関係なく実行したい処理を記述 | 省略可能 |
try と except は必ず必要ですが、else と finallyは省略可能です。
finally は省略されることも多いのですが、例えばファイルやデータベースの読み書きなどで、必ずクローズ処理が必要な場合は、finally に記述します。
一方、 else は 他の言語で実装されていないケースも多く、あまり使いどころは無いかと思います。
では、具体的な例を見ていきましょう。
下記は 100 を cnt の値で除算するプログラムなのですが、事前に cnt に 0 を代入しているため、「ゼロ除算」のエラー(例外)が発生します。
1 2 3 4 5 6 7 8 |
try: cnt = 0 val = 100 / cnt except: print("例外が発生しました") finally: print("処理が終了しました") |
このプログラムの実行結果は次のようになります。
全ての例外を1か所で処理する場合
先ほど提示したプログラム例のように、except だけで全ての例外を受取ることが出来ます。
受け取った例外の中身が知りたい場合は、exception の代わりに except Exception as e: と記述することで、例外のメッセージを取得することが出来ます。
1 2 3 4 5 6 7 8 9 10 |
try: cnt = 0 val = 10 /cnt except Exception as e: if e.args[0] == 'division by zero': print("ゼロ除算") else: print("その他のエラー") finally: print("処理が終了しました") |
e は例外発生時に一時的に生成される変数であり、名前は何でも構いませんが、ここでは慣例的に e という名前を使用しました。
e の args プロパティにはタプル形式で例外情報が渡される仕様になっており、最初の要素(args[0])
に例外処理を表すメッセージが格納されています。
ちなみに、ゼロ除算の場合は 'diviision by zero' という文字列が渡されます。
尚、Pythonで全ての例外を except で受け取る方法は推奨されていません。
指定した例外のみを処理する場合
指定した例外のみを処理する場合は、except に続けて例外の名前を指定します。
尚、この方法で1つでも例外を指定してしまうと、ここで記述していない他の例外については例外処理がされなくなります。
発生する可能性がある例外は全て記述するようにしましょう。
とはいうものの、想定外の例外が発生しても停止させたくない場合もあります。
その時は、except Exception: を記述することで、漏れなく例外処理を行う事が出来ます。
ここは except: と記述しても良さそうな気がしますが、必ず except Exception: と記述する必要があります。
尚、例外は上から順番に処理されます。
except Exception: を最初にもってくると、そこで全ての例外を処理してしまうことになり、個別の例外処理ができなくなります。
except Exception: は必ず 個別の exception の最後に記述して下さい。
1 2 3 4 5 6 7 8 9 10 11 12 |
try: cnt = 0 val = 10 /cnt except ZeroDivisionError: print("ゼロ除算") except FileNotFoundError: print("ファイルが見つかりません") except Exception: print("エラー") finally: print("処理が終了しました") |
個別の例外処理においても、例外の詳細情報を取得することが出来ます。
except 例外名 as e: と記述することで、e という変数の args プロパティを使って、詳細情報を参照できます。
例えば、ゼロ除算の例外だと、次の様に記述します。
1 |
except ZeroDivisionError as e: |
例外処理の階層化
例外処理が実装された上位の処理から子供の処理を呼び出す場合、その子供の処理にも例外処理が実装されているなら、例外処理の親子関係=階層化が成り立ちます。
通常のプログラムにおいて例外処理を階層化することはほとんど無いですが、共通クラスのような汎用的なものを利用する場合は、その中に例外処理が実装されていることもあるので、無意識に階層化になるケースも多々あります。
例外処理が階層化された場合に問題となるのが、子の処理で処理した例外を、呼び出し元に通知するか否かです。この辺は作る対象物の仕様やプログラミングの考え方によってどちらも有り得るため、ここではそれぞれの方法を解説しておきます。
関数内で発生した特定の例外は呼び出し元に通知しない
例えば、次のプログラムを実行しすると calc(5,0) により0除算が発生します。
しかし、関数内でゼロ除算( ZeroDivisionError) の例外をキャッチして処理(今回はprintの実行)しているため、ゼロ除算発生時だけは呼び出し元に通知されません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#関数 def calc(val,cnt): try: res = val / cnt return res except ZeroDivisionError: print("ゼロ除算") #メイン処理 try: print("関数の実行") calc(5,0) print("関数の終了") except: print("関数でエラー発生") |
結果は以下の通りで、関数内でゼロ除算の例外が処理され、呼び出し元にはエラーが通知されなかったことが分かります。
関数やメソッド内で例外処理を完結させ、呼び出し元には何も通知したくない場合、この方法を使います。
もしゼロ除算が発生しても無視したい場合は、処理として pass を記述します。こうすることで何も処理されず、且つ呼び出し元にも通知がされません。
1 2 3 4 5 6 7 |
#関数 def calc(val,cnt): try: res = val / cnt return res except ZeroDivisionError: pass |
関数内で発生した例外を、関数の戻り値として返す
関数内で発生した全ての例外を関数の戻り値として返す場合、関数内の except: の次の行に return と戻り値を記述します。
return に何も値を指定しない場合、None が返されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#関数 def calc(val,cnt): try: res = val / cnt return res except: return 0 #<=== 全ての例外発生時 0 を返す #メイン処理 try: print("関数の実行") calc(5,0) print("関数の終了") except: print("関数でエラー発生") |
結果は次の通りで、どんな例外が発生しても、呼び出し元には通知されません。
関数内で何らかの例外が発生した場合、関数の戻り値として呼び出し元に返したい場合によく利用されます。
return の代わりに pass を指定すると、全ての例外が握りつぶされたようになり、後からバグを見つけにくくなるので注意ましょう。
関数内の例外を処理しつつ、呼び出し元にも通知する
関数やメソッド内で例外を処理しつつ、呼び出し元にも例外が発生したことを知らせたい場合があります。その時は、例外処理の中で raise と1行記述するだけで、呼び出し元にも例外を知らせる事が可能です。
下記は、サンプルプログラムのゼロ除算の例外処理に、raise を入れたものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#関数 def calc(val,cnt): try: res = val / cnt return res except ZeroDivisionError as e: print("ゼロ除算") raise #<=== 例外を呼び出し元に知らせる #メイン処理 try: print("関数の実行") calc(5,0) print("関数の終了") except: print("関数でエラー発生") |
これを実行すると以下の結果になります。
関数内で「ゼロ除算」が処理された後で、呼び出し元にも例外が通知され、「関数でエラーが発生」が出力されていますね。
独自の例外を作る
例外は新しいものを自作することが可能です。
例外発生ごとにエラーメッセージ指定する
例外の受け取り側に任意のエラーメッセージを渡せば良いだけであれば、Exceptionクラスを継承した中身が空のクラスを用意するだけです。
例えば、MyZeroErrorという名前の独自例外クラスを作るには、次の様に記述します。
1 2 3 |
#Exceptionクラスを継承した中身が空の自作例外クラス class MyZeroError(Exception): pass |
この自作例外クラスを使って例外を発生させるには、raise を使って次のように記述します。
1 |
raise MyZeroError(任意のエラーメッセージ) |
下記は、calc 関数の中で cnt が 0 なら、MyZeroErrorのインスタンスに "ゼロは指定できません" というエラーメッセージを渡し、 raise をする例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#Exceptionクラスを継承した中身が空の自作例外クラス class MyZeroError(Exception): pass #cnt=0の時、自作の例外を発生させる def calc(val,cnt): res = None if cnt == 0: raise MyZeroError("ゼロは指定できません") else: res = val / cnt return res #メイン処理 try: print("関数の実行") calc(5,0) print("関数の終了") except Exception as e: print("関数でエラー発生") print(e) |
このプログラムを実行すると、下記のように出力されます。
関数の実行後、関数内部でエラーが発生し、 呼び出し元の except Exception as e でエラーメッセージが受け取れていることが分かります。
MyzeroError は中身が空ですが、Exception クラスを継承しており、Exceptionクラスからインスタンス生成を生成する際の引数(コンストラクタへの引数)はエラーメッセージとして扱われる仕様になっているため、 MyzeroError に渡したエラーメッセージが呼び出し元で受け取れるのです。
例外発生時に表示したいエラーメッセージを埋め込む
例外発生時にエラーメッセージを指定するのではなく、独自例外クラスの中にエラーメッセージを埋め込んでおきたい場合は、 __str__ メソッドの戻り値として固定メッセージを記述します。
1 2 3 |
class MyZeroError(Exception): def __str__(self): return 埋め込みたいエラーメッセージ |
下記はこのMyZeroErrorをに対して、 'ゼロ除算が発生しました' をエラーメッセージとして埋め込んだ例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class MyZeroError(Exception): def __str__(self): return 'ゼロ除算が発生しました' def calc(val,cnt): res = None if cnt == 0: raise MyZeroError() else: res = val / cnt return res try: print("関数の実行") calc(5,0) print("関数の終了") except Exception as e: print("関数でエラー発生") print(e) |
このプログラムを実行すると、下記の結果が表示されます。
例外発生時 raise MyZeroError() で呼び出しましたが、MyZeroErrorの中に埋め込んだ固定メッセージ「 ゼロ除算が発生しました」がちゃんと表示されています。
例外発生時に指定した値を、埋め込んだエラーメッセージに反映する
独自例外クラスに埋め込んだエラーメッセージに対して、例外発生時に指定した値を反映させたい場合、次の独自例外クラスを作成します。
1 2 3 4 5 |
class MyZeroError(Exception): def __init__(self,arg): self.arg = arg def __str__(self): return f'{self.arg} は指定できません' |
__str__ メソッドは、インスタンスに対して str() や print() などで文字列化が要求された時に呼ばれる特殊メソッドであり、コンストラクタで渡された arg を埋め込まれたメッセージと結合しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class MyZeroError(Exception): def __init__(self,arg): self.arg = arg def __str__(self): return f'{self.arg} は指定できません' def calc(val,cnt): res = None if cnt == 0: raise MyZeroError(0) else: res = val / cnt return res try: print("関数の実行") calc(5,0) print("関数の終了") except Exception as e: print("関数でエラー発生") print(e) |
結果は下記の通りですが、 raise MyZeroError(0) で指定した 0 が、MyZeroError 内部の固有メッセージと結合され、 '0 は指定できません' というエラーメッセージとして呼び出し元に通知されていることが分かります。
例外処理の指針
例外とは「通常ではない状態」あるいは「想定していな状態」を意味しています。
つまり、想定している内容は例外にすべきではないという解釈が成り立ちます。
かと言って、想定していたとしても、プログラムでは回避できない場合もあります。
では、どんな時に例外処理をすればよいのでしょう。
賛否両論はあるかもしれませんが個人的には次のように考えています。
例外にしてはいけない場合 | 明らかにエラーとなる原因が分かっていて、 プログラムの処理で解決できる場合は プログラムの条件判断で処理する。 例えば次のものは例外処理を使わない。 ①ファイルやテーブルが存在しない場合はプログラムで事前にチェックが可能 ②エラーの要因となる値が含まれるデータは、プログラムで削除又は補間が可能 ③誤った入力値はプログラムで判断が可能 |
例外にしてよい場合 | OSに起因するエラーはプログラムでは解決できないため、例外処理にする。 ①メモリ不足 ②ファイルシステム関連エラー(ロックが掛けられた、書き込み禁止など) ③I/O関連エラ(ディスク障害、通信エラー、DB接続エラーなど) |
上記以外にも、例外は毎回例外クラスのインスタンスを生成するため、ループ処理の中で大量の例外を処理すると、速度低下を招く恐れがあります。
また、何でもかんでも例外処理してしまうと、エラーの原因が隠れてしまったり、プログラムが分かり難くなる可能性があることも、留意しておきましょう。
例外処理一覧
どの様な例外があるかについては、公式サイトの「組み込み例外」に詳細が記載されていますが、以下はそれを要約した一覧です。
一般的な例外
例外 | 内容 |
---|---|
AssertionError | assert文に指定した条件式がFalseの場合に発生。 |
AttributeError | 属性の参照や代入が失敗した場合に発生。 |
EOFError | input()がデータを読まず、 end-of-file (EOF) に達した場合に発生。 |
FloatingPointError | 現在は未使用。 |
GeneratorExit | ジェネレータやコルーチンが閉じられたときに発生。 |
ImportError | import文でのモジュールのロード失敗時に発生。 |
ModuleNotFoundError | ImportErrorのサブクラスで、import文でモジュールが見つからない場合に発生。 |
IndexError | インデックスの添字が範囲外の場合に発生。 |
KeyError | 指定されたキーが辞書内に見つからなかった場合に発生。 |
KeyboardInterrupt | ユーザが割り込みキー (通常はControl-CまたはDelete) を押した場合に送出されます。実行中、割り込みは定期的に監視されます。 |
MemoryError | 処理中に発生したメモリ不足が、オブジェクトの消去等で復旧の可能性がある場合に発生。 |
NameError | ローカルまたはグローバルの名前が見つからなかった場合に発生。 |
NotImplementedError | 派生クラスで抽象クラスのメソッドがオーバライドされていなかった場合に発生。 |
OSError([arg]) | システム関数がシステム関連のエラーを返した場合に発生。 |
OverflowError | 算術演算の結果が桁あふれを起こした場合に発生。 |
RecursionError | インタープリタが最大再帰深度の超過を検出した時に発生。 |
ReferenceError | ガーベジコレクションによって回収された後のオブジェクトにアクセスした場合に発生。 |
RuntimeError | 他のカテゴリに分類できないエラーが検出された場合に発生。 |
StopIteration | 組込み関数next()と__next__()メソッドによって、そのイテレータが生成するアイテムが尽きた場合に発生。 |
StopAsyncIteration | イテレーションを停止するために、asynchronous iteratorオブジェクトの__anext__()メソッドによって返される必要があります。 |
SyntaxError | 構文エラーに遭遇した場合に送出されます。 |
IndentationError | 正しくないインデントが見つかった場合に発生。 |
TabError | タブとスペースを一貫しない方法でインデントとして使っている場合に発生。 |
SystemError | インタプリタが深刻ではない内部エラーを発見した場合に発生。 |
SystemExit | sys.exit()関数を呼び出した場合に発生。 |
TypeError | 組み込み演算または関数が適切でない型のオブジェクトに対して適用された場合に発生。 |
UnboundLocalError | 参照した変数に値が代入されていなかった場合に発生。 |
UnicodeError | Unicode に関するエンコードまたはデコードのエラーが発生した場合に発生。 |
UnicodeEncodeError | Unicode 関連のエラーがエンコード中に発生した場合に発生。 |
UnicodeDecodeError | Unicode 関連のエラーがデコード中に発生した場合に発生。 |
UnicodeTranslateError | Unicode 関連のエラーが変換中に発生した場合に発生。 |
ValueError | 演算子や関数が、適切でない値を持つ引数を受け取った場合に発生。 |
ZeroDivisionError | 0 で除算した場合に発生。 |
OS関連の例外一覧
例外 | 内容 |
---|---|
BlockingIOError | ある操作が、ノンブロッキング操作に設定されたオブジェクト (例えばソケット) をブロックしそうになった場合に送出されます。 |
ChildProcessError | 子プロセスの操作が失敗した場合に送出されます。 |
ConnectionError | コネクション関係の問題の基底クラス。 |
BrokenPipeError | ConnectionErrorのサブクラスで、もう一方の端が閉じられたパイプに書き込こもうとするか、書き込みのためにシャットダウンされたソケットに書き込こもうとした場合に発生します。 |
ConnectionAbortedError | ConnectionErrorのサブクラスで、接続の試行が通信相手によって中断された場合に発生します。 |
ConnectionRefusedError | ConnectionErrorのサブクラスで、接続の試行が通信相手によって拒否された場合に発生します。 |
ConnectionResetError | ConnectionErrorのサブクラスで、接続が通信相手によってリセットされた場合に発生します。 |
FileExistsError | すでに存在するファイルやディレクトリを作成しようとした場合に送出されます。 |
FileNotFoundError | 要求されたファイルやディレクトリが存在しない場合に送出されます。 |
InterruptedError | システムコールが入力信号によって中断された場合に送出されます。 |
IsADirectoryError | ディレクトリに (os.remove()などの) ファイル操作が要求された場合に送出されます。 |
NotADirectoryError | ディレクトリ以外のものに (os.listdir()などの) ディレクトリ操作が要求された場合に送出されます。 |
PermissionError | 十分なアクセス権、例えばファイルシステム権限のない操作が試みられた場合に送出されます。 |
ProcessLookupError | 与えられたプロセスが存在しない場合に送出されます。 |
TimeoutError | システム関数がシステムレベルでタイムアウトした場合に送出されます。 |
まとめ
今回は Python の例外処理について、使い方と注意点について解説しました。
趣味で使うちょっとしたプログラムであれば、例外処理は入れなくても問題はありませんが、ある程度の規模になったり、第三者が使うようなプログラムでは、例外処理を意識する必要があります。
特にユーザーの操作が伴うプログラムにおいては、入力したデータが途中で消えたり、壊れてしまうような事態は避けなければなりません。
ただ、何でもかんでも例外処理にしてしまうと、処理速度を低下させたり、プログラムの動きが分かり難くなってしまうので、その点だけ注意して活用して頂ければと思います。
今回の記事が皆さんのお役に立てば幸いです。
コメント