Pythonの例外処理(try, except, else, finally)

Modified: | Tags: Python, エラー処理

Pythonで例外(実行中に検出されたエラー)をキャッチして処理するにはtry, exceptを使う。例外が発生しても途中で終了させずに処理を継続できる。さらにelse, finallyを使うと終了時の処理を設定可能。

Pythonの主なエラーメッセージとその対策のまとめについては以下の記事を参照。

assert文でデバッグ用のアサーションを設定することも可能。

例外処理の基本: try, except

例えばゼロによる除算が行われるとZeroDivisionErrorという例外が発生して処理が終了する。

# print(1 / 0)
# ZeroDivisionError: division by zero

この例外をキャッチ(捕捉)するには以下のように記述する。

try:
    print(1 / 0)
except ZeroDivisionError:
    print('Error')
# Error

さらに、except 例外名 as 変数名:とすると、変数に例外オブジェクトを格納して使用できる。変数名は任意の名前を指定できるが、eerrといった名前が使われることが多い。

例外オブジェクトには例外発生時に出力されるエラーメッセージなどが格納されており、エラーの内容を確認できる。

try:
    print(1 / 0)
except ZeroDivisionError as e:
    print(e)
    print(type(e))
# division by zero
# <class 'ZeroDivisionError'>

なお、except 例外名 as 変数名:はPython3での書き方で、Python2ではexcept 例外名, 変数名:と書く。

基底クラスを指定してもキャッチされる。例えば、ZeroDivisionErrorの基底クラスはArithmeticError。変数には実際に発生した派生クラスの例外オブジェクトが格納される。

print(issubclass(ZeroDivisionError, ArithmeticError))
# True

try:
    print(1 / 0)
except ArithmeticError as e:
    print(e)
    print(type(e))
# division by zero
# <class 'ZeroDivisionError'>

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.9.16 ドキュメント

def divide_wildcard(a, b):
    try:
        print(a / b)
    except:
        print('Error')

divide_wildcard(1, 0)
# Error

divide_wildcard('a', 'b')
# Error

この場合、すべての例外をキャッチするので、SystemExitsys.exit()などが送出)、KeyboardInterrupt(割り込みキーCtrl + C入力で送出)もキャッチしてしまう。多くの場合、これらの例外はキャッチせずそのままプロセスを終了するほうが望ましいため、次に紹介するExceptionを使うほうがよい。

基底クラスException

システム終了(SystemExit, KeyboardInterruptなど)以外のすべての組み込み例外の基底クラスであるExceptionexcept節に指定する方法がある。

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
 ├── BaseExceptionGroup
 ├── GeneratorExit
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── StopAsyncIteration
      ├── StopIteration
      ├── ...
       ...

SystemExitKeyboardInterruptExceptionを継承していないため、except節にExceptionを指定した場合はsys.exit()や割り込みキー入力の例外をキャッチせずそのままプロセスが終了する。

この例外は sys.exit() 関数から送出されます。Exception をキャッチするコードに誤ってキャッチされないように、Exception ではなく BaseException を継承しています。これにより例外は上の階層に適切に伝わり、インタープリタを終了させます。 組み込み例外 - SystemExit — Python 3.11.3 ドキュメント

ユーザが割り込みキー (通常は Control-C または Delete) を押した場合に送出されます。実行中、割り込みは定期的に監視されます。Exception を捕捉するコードに誤って捕捉されてインタプリタの終了が阻害されないように、この例外は BaseException を継承しています。 組み込み例外 - KeyboardInterrupt — Python 3.11.3 ドキュメント

SystemExitなども含むすべての組み込み例外の基底クラスはBaseExceptionexcept節に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文は何もしない文。詳細は以下の記事を参照。

具体例: 画像ファイルの読み込み・処理

例外処理を用いると便利な例として、画像ファイルの読み込み・処理がある。

以下はフォルダ内の画像ファイルをPillowを使って一括でリサイズする例。

例外処理を用いない場合。フォルダ内のすべてのファイルパスをglob()で抽出し、特定の拡張子に一致したものだけを処理する。

import os
import glob
from PIL import Image

dst_dir = 'data/temp/images_half'
os.makedirs(dst_dir, exist_ok=True)
files = glob.glob('./data/temp/images/*')

for f in files:
    root, ext = os.path.splitext(f)
    if ext in ['.jpg', '.png']:
        img = Image.open(f)
        img_resize = img.resize((img.width // 2, img.height // 2))
        basename = os.path.basename(root)
        img_resize.save(os.path.join(dst_dir, basename + '_half' + ext))

画像ファイルは様々な拡張子があるため、抜け漏れなくすべてを指定するのは大変。そこで例外処理を用いると以下のようになる。

files = glob.glob('./data/temp/images/*')

for f in files:
    try:
        img = Image.open(f)
        img_resize = img.resize((img.width // 2, img.height // 2))
        root, ext = os.path.splitext(f)
        basename = os.path.basename(root)
        img_resize.save(os.path.join(dst_dir, basename + '_half' + ext))
    except OSError as e:
        pass

PillowのImage.open()で開けるファイルはすべて処理できる。

前者のように処理の前に明示的に条件判定を行うスタイルは「LBYL: Look Before You Leap(転ばぬ先の杖)」、後者のように例外処理を用いるスタイルは「EAFP: Easier to Ask for Forgiveness than Permission(認可をとるより許しを請う方が容易)」と呼ばれる。

どちらが優れているというわけではないが、ファイルを開くような処理や、ネットワークを介した失敗する可能性がある処理などは、例外処理を使うと簡潔に書ける場合がある。

関連カテゴリー

関連記事