Pythonの例外処理(try, except, else, finally)
Pythonで例外(実行中に検出されたエラー)をキャッチして処理するにはtry
, except
を使う。例外が発生しても途中で終了させずに処理を継続できる。さらにelse
, finally
を使うことで終了時の処理を設定することが可能。
ここでは以下の内容について説明する。
- 例外処理の基本:
try
,except
- 複数の例外をキャッチ
- 複数の例外に異なる処理を実行
- 複数の例外に同じ処理を実行
- すべての例外をキャッチ
- ワイルドカードの
except
節(bare except) - 基底クラス
Exception
- ワイルドカードの
- 正常終了時の処理:
else
- 終了時に常に行う処理:
finally
- 例外を無視:
pass
- 具体例: 画像ファイルの読み込み・処理
Pythonの主なエラーメッセージとその対策のまとめについては以下の記事を参照。
例外処理の基本: try, except
例えばゼロによる除算が行われるとZeroDivisionError
という例外が発生して処理が終了する。
# print(1 / 0)
# ZeroDivisionError: division by zero
この例外をキャッチ(捕捉)するには以下のように記述する。
try:
print(1 / 0)
except ZeroDivisionError:
print('Error')
# Error
さらに、except 例外名 as 変数名:
とすることで、変数に例外オブジェクトを格納して使用することができる。変数名は任意の名前を指定できるが、e
やerr
といった名前が使われることが多い。
例外オブジェクトには例外発生時に出力されるエラーメッセージなどが格納されており、それを出力することでエラーの内容を確認できる。
try:
print(1 / 0)
except ZeroDivisionError as e:
print(e)
print(type(e))
# division by zero
# <class 'ZeroDivisionError'>
なお、except 例外名 as 変数名:
はPython3での書き方で、Python2ではexcept 例外名, 変数名:
と書く。
Python組み込みの例外名の一覧は以下の公式ドキュメントを参照。
try
節で例外が発生した時点でtry
節の中のそれ以降の処理はスキップされる。以下の例のようにfor
ループの途中で例外が発生するとその時点でfor
ループは終了してexcept
節の処理に移行する。
try:
for i in [-2, -1, 0, 1, 2]:
print(1 / i)
except ZeroDivisionError as e:
print(e)
# -0.5
# -1.0
# division by zero
正常終了時の処理や終了時に必ず行う処理などは後述のelse
節やfinally
節で指定する。
複数の例外をキャッチ
除算を行いZeroDivisionError
をキャッチする以下の関数を定義する。
def divide(a, b):
try:
print(a / b)
except ZeroDivisionError as e:
print('catch ZeroDivisionError:', e)
ZeroDivisionError
はキャッチできるが、それ以外の例外はキャッチできず処理が途中終了する。
divide(1, 0)
# catch ZeroDivisionError: division by zero
# divide('a', 'b')
# TypeError: unsupported operand type(s) for /: 'str' and 'str'
複数の例外をキャッチして処理を実行する方法を説明する。
複数の例外に異なる処理を実行
except
節は複数指定することができる。複数の例外に対してそれぞれ異なる処理を記述可能。
def divide_each(a, b):
try:
print(a / b)
except ZeroDivisionError as e:
print('catch ZeroDivisionError:', e)
except TypeError as e:
print('catch TypeError:', e)
divide_each(1, 0)
# catch ZeroDivisionError: division by zero
divide_each('a', 'b')
# catch TypeError: unsupported operand type(s) for /: 'str' and 'str'
複数の例外に同じ処理を実行
一つのexcept
節に複数の例外名をタプルで指定することもできる。
変数に格納される例外オブジェクトの中身はそれぞれ異なるので、エラーメッセージを出力してエラーの内容を確認するだけであればこれで問題ない。
def divide_same(a, b):
try:
print(a / b)
except (ZeroDivisionError, TypeError) as e:
print(e)
divide_same(1, 0)
# division by zero
divide_same('a', 'b')
# unsupported operand type(s) for /: 'str' and 'str'
すべての例外をキャッチ
基本的には想定される例外名をexcept
節に指定すべきだが、特定の例外を指定せずにすべての例外をキャッチすることも可能。
ワイルドカードのexcept節(bare except)
except
節から例外名を省略するとすべての例外をキャッチできる。複数のexcept
節がある場合は例外名を省略できるのは最後のexcept
節のみ。
例外名を省略した書き方はワイルドカードのexcept
節やbare exceptなどと呼ばれるが、公式ドキュメントにあるように使用には注意が必要。
最後の except 節では例外名を省いて、ワイルドカード (wildcard、総称記号) にすることができます。ワイルドカードの except 節は非常に注意して使ってください。というのは、ワイルドカードは通常のプログラムエラーをたやすく隠してしまうからです!
8. エラーと例外 — Python 3.8.5 ドキュメント
def divide_wildcard(a, b):
try:
print(a / b)
except:
print('Error')
divide_wildcard(1, 0)
# Error
divide_wildcard('a', 'b')
# Error
この場合、すべての例外をキャッチするので、SystemExit
(sys.exit()
などが送出)、KeyboardInterrupt
(割り込みキーCtrl + C
入力で送出)もキャッチしてしまう。多くの場合、これらの例外はキャッチせずそのままプロセスを終了するほうが望ましいため、次に紹介するException
を使うほうがよい。
基底クラスException
システム終了(SystemExit
, KeyboardInterrupt
など)以外のすべての組み込み例外の基底クラスであるException
をexcept
節に指定する方法がある。
def divide_exception(a, b):
try:
print(a / b)
except Exception as e:
print(e)
divide_exception(1, 0)
# division by zero
divide_exception('a', 'b')
# unsupported operand type(s) for /: 'str' and 'str'
組み込み例外のクラス階層は以下の通り。
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ...
...
SystemExit
やKeyboardInterrupt
はException
を継承していないため、except
節にException
を指定した場合はsys.exit()
や割り込みキー入力の例外をキャッチせずそのままプロセスが終了する。
この例外は sys.exit() 関数から送出されます。Exception をキャッチするコードに誤ってキャッチされないように、Exception ではなく BaseException を継承しています。これにより例外は上の階層に適切に伝わり、インタープリタを終了させます。 組み込み例外 - SystemExit — Python 3.8.5 ドキュメント
ユーザが割り込みキー (通常は Control-C または Delete) を押した場合に送出されます。実行中、割り込みは定期的に監視されます。Exception を捕捉するコードに誤って捕捉されてインタプリタの終了が阻害されないように、この例外は BaseException を継承しています。 組み込み例外 - KeyboardInterrupt — Python 3.8.5 ドキュメント
SystemExit
なども含むすべての組み込み例外の基底クラスはBaseException
。except
節にException
ではなくBaseException
を指定すると、ワイルドカード(例外名を省略)と同じくSystemExit
などもキャッチされるようになる。
なお、想定していない例外までキャッチしてしまうのはバグの温床になるので、繰り返しになるが、except
節には可能な限り想定される例外名を指定したほうがいい。
正常終了時の処理: else
try
節で例外が発生せず正常終了したあとに行う処理をelse
節に指定できる。例外が発生してexcept
でキャッチした場合はelse
節の処理は実行されない。
def divide_else(a, b):
try:
print(a / b)
except ZeroDivisionError as e:
print('catch ZeroDivisionError:', e)
else:
print('finish (no error)')
divide_else(1, 2)
# 0.5
# finish (no error)
divide_else(1, 0)
# catch ZeroDivisionError: division by zero
終了時に常に行う処理: finally
例外が発生した場合もしなかった場合も常に最後に行う処理をfinally
節に指定できる。
def divide_finally(a, b):
try:
print(a / b)
except ZeroDivisionError as e:
print('catch ZeroDivisionError:', e)
finally:
print('all finish')
divide_finally(1, 2)
# 0.5
# all finish
divide_finally(1, 0)
# catch ZeroDivisionError: division by zero
# all finish
else
節とfinally
節を同時に使うことも可能。正常終了時はelse
節の処理が実行されたあとにfinally
節の処理が実行される。
def divide_else_finally(a, b):
try:
print(a / b)
except ZeroDivisionError as e:
print('catch ZeroDivisionError:', e)
else:
print('finish (no error)')
finally:
print('all finish')
divide_else_finally(1, 2)
# 0.5
# finish (no error)
# all finish
divide_else_finally(1, 0)
# catch ZeroDivisionError: division by zero
# all finish
例外を無視: pass
例外をキャッチしても特に何も処理を行わずにスルーしたい場合はpass
文を使う。
def divide_pass(a, b):
try:
print(a / b)
except ZeroDivisionError:
pass
divide_pass(1, 0)
pass
文は何もしない文。詳細は以下の記事を参照。
- 関連記事: Pythonのpass文の意味と使い方
具体例: 画像ファイルの読み込み・処理
例外処理を用いると便利な例として、画像ファイルの読み込み・処理がある。
以下はフォルダ内の画像ファイルをPillowを使って一括でリサイズする例。
例外処理を用いない場合。フォルダ内のすべてのファイルパスをglob()
で抽出し、特定の拡張子に一致したものだけを処理する。
- 関連記事: Pythonで条件を満たすパスの一覧を再帰的に取得するglobの使い方
- 関連記事: Pythonでパス文字列からファイル名・フォルダ名・拡張子を取得、結合
- 関連記事: Pythonのin演算子でリストなどに特定の要素が含まれるか判定
import os
import glob
from PIL import Image
files = glob.glob('./data/temp/images/*')
for f in files:
title, ext = os.path.splitext(f)
if ext in ['.jpg', '.png']:
img = Image.open(f)
img_resize = img.resize((int(img.width / 2), int(img.height / 2)))
img_resize.save(title + '_half' + ext)
画像ファイルは様々な拡張子があるため、抜け漏れなくすべてを指定するのは大変。そこで例外処理を用いると以下のようになる。
files = glob.glob('./data/temp/images/*')
for f in files:
try:
img = Image.open(f)
img_resize = img.resize((int(img.width / 2), int(img.height / 2)))
title, ext = os.path.splitext(f)
img_resize.save(title + '_half' + ext)
except OSError as e:
pass
PillowのImage.open()
で開けるファイルはすべて処理できる。
前者のように処理の前に明示的に条件判定を行うスタイルは「LBYL: Look Before You Leap(転ばぬ先の杖)」、後者のように例外処理を用いるスタイルは「EAFP: Easier to Ask for Forgiveness than Permission(認可をとるより許しを請う方が容易)」と呼ばれる。
どちらが優れているというわけではないが、ファイルを開くような処理や、ネットワークを介した失敗する可能性がある処理などは、例外処理を使うと簡潔に書ける場合がある。