Python 3 の例外システムを活用する上で押さえておきたいポイントをまとめました。
例外システムに関しては過去に「 Python の例外処理」という記事も書いています。この記事と内容が重複しますが、例外に興味のある方はよろしければそちらもご覧ください。
目次
本記事の目次です。
- 基本形を押さえる
finally
を押さえるelse
を押さえる- 組み込みの例外クラスのツリーを押さえる
except
のパターンを押さえる- 例外オブジェクトのアトリビュートを利用する
- コンテキストマネージャを使う
順に見ていきましょう。
基本形を押さえる
Python 3 の例外処理の基本形は次のとおりです。
import sys
FILE_IN = 'sample.dat'
try:
# 例外が起こる可能性のある処理
file = open(FILE_IN)
except Exception:
# 例外が起こったときの処理
print('ファイル {} を開くことができません。'.format(FILE_IN), file=sys.stderr)
sys.exit(1)
キーワード try
と except
を組み合わせて使います。
他の言語を知る方だと try
catch
のパターンに馴染みのある方が多いでしょうか。 Python の場合は catch
の代わりに except
を使用します。
except
句は一般に次のいずれかのパターンで書きます。
except キャッチしたい例外のクラス:
except キャッチしたい例外のクラス as キャッチされた例外オブジェクトにつける名前:
finally
を押さえる
try
と except
の後には finally
という句を繋げることができます(厳密にいうと except
がなくても大丈夫です)。
import sys
FILE_IN = 'sample.dat'
try:
# 例外が起こる可能性のある処理
file = open(FILE_IN)
file.close()
except Exception:
# 例外が起こったときの処理
print('ファイル {} を開くことができません。'.format(FILE_IN), file=sys.stderr)
sys.exit(1)
finally:
# 例外の発生有無にかかわらず最後に実行したい処理
print('finally です。')
# =>
# ファイル sample.dat を開くことができません。
# finally です。
finally
句は例外が発生してもしなくても必ず実行されます。
finally
はよくできていて、処理がどの経路を通っても必ず実行され、かつ、なるだけ遅いタイミングで実行されるようになっています。異なる経路のパターン a) - e) をあげて、どのタイミングで finally
句が実行されるのかを以下に見ていきましょう。
a) try
の中で例外が発生しなかった場合 → try
を抜けるときに実行される
try:
print('try です。')
finally:
print('finally です。')
# =>
# try です。
# finally です。
b) try
の中で例外が発生し、 except
句のひとつでキャッチされ、その中で例外があげられなかった場合 → except
句の後に実行される
try:
value = 1 / 0
except ZeroDivisionError:
print('ZeroDivisionError です。')
finally:
print('finally です。')
# =>
# ZeroDivisionError です。
# finally です。
c) try
の中で例外が発生し、 except
句のひとつでキャッチされ、その中で例外があげられた場合 → except
句内で例外があげられる直前で実行される
try:
value = 1 / 0
except ZeroDivisionError as e:
print('ZeroDivisionError です。')
raise e
finally:
print('finally です。')
# =>
# ZeroDivisionError です。
# finally です。
# Traceback (most recent call last):
# ...
# ZeroDivisionError: division by zero
d) try
の中で例外が発生し、どの except
句でもキャッチされなかった場合 → finally
句が実行された後に例外があがる(公式ドキュメントでは「 re-raised 」という表現が使われています)
try:
value = 1 / 0
except StopIteration:
print('StopIteration です。')
finally:
print('finally です。')
# =>
# finally です。
# Traceback (most recent call last):
# ...
# ZeroDivisionError: division by zero
e) try
内で return
文や break
文が来て try
句を抜ける場合 → return
や break
の後に実行される
def sample_exception():
try:
print('try です。')
return True
except:
return False
finally:
print('finally です。')
result = sample_exception()
print(result)
# =>
# try です。
# finally です。
# True
このように finally
句はどんな場合でも必ず実行されるので、 try
句の中の処理の成否によらずに必ず実行したい処理――例えばデータベース等の外部リソースの開放等をするのに有用です。
else
を押さえる
try
except
句の後には else
という句を繋げることができます。 else
句は try
句の中で 例外が起こらなかったときにのみ 実行されます。
import sys
FILE_IN = 'sample.dat'
try:
# 例外が起こる可能性のある処理
file = open(FILE_IN)
file.close()
except Exception:
# 例外が起こったときの処理
print('ファイル {} を開くことができません。'.format(FILE_IN), file=sys.stderr)
sys.exit(1)
else:
# 例外が起こらなかったときの処理
print('file.closed: {}'.format(file.closed))
# =>
# file.closed: True
例外が起こらなかったときにのみ実行されると聞くと、「 else
句の中身を try
の中に書けば else
句は要らないんじゃないか」と思えますが(私は思いました)、 else
は try
の中身を最小限に留め思わぬ例外処理が起こってしまうのを防ぐのに有効です。 else
句が便利なパターンとして例えば次のような使い方が考えられます。
try:
# データベースに変更を加える処理
except:
# データベースの処理が失敗したのでトランザクションをロールバックする
else:
# 処理が成功した場合にログを残す
個人的には、この「 try
の処理が成功した場合に実行される句」に else
という名前を使うのは違和感があります。 then
等の名前の方が直感的でわかりやすい気がしますが、このあたりは「予約語をなるべく少なくする」ことを優先した判断の結果なのですかね。
組み込みの例外クラスのツリーを押さえる
Python 3 には組み込みの例外クラスが多数用意されています。子クラスの例外は親クラスの except
句でキャッチできるので、適切な粒度で例外をキャッチして正しく例外処理を行うために、このツリー構造を押さえて必要があります。
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
# ZeroDivisionError は親クラスの ArithmeticError でキャッチできる
try:
value = 1 / 0
except ArithmeticError as e:
print('例外が起こりました。')
print('{}: {}'.format(type(e).__name__, str(e)))
# =>
# 例外が起こりました。
# ZeroDivisionError: division by zero
except
のパターンを押さえる
try
句の後に書く except
句はさまざまなパターンで記述することができます。
ひとつの except
句で複数の例外をキャッチする:
try:
value = 1 / 0
except (OverflowError, ZeroDivisionError) as e:
print('例外が起こりました。')
print('{}: {}'.format(type(e).__name__, str(e)))
# =>
# 例外が起こりました。
# ZeroDivisionError: division by zero
複数の except
句をひとつの try
句につける:
try:
value = 1 / 0
except OverflowError as e:
print('OverflowError です。')
except ZeroDivisionError as e:
print('ZeroDivisionError です。')
except Exception as e:
print('Exception です。')
# =>
# 例外が起こりました。
# ZeroDivisionError: division by zero
except
句を複数連ねるときの注意点は、例外の場合分けを適切にするためには「 小さい例外を前に、大きな例外を後に書かなくてはいけない 」ことです。
上のサンプルではこのとおりに「小さい例外を先、大きな例外を後」にして書いていますが、これを変えて Exception
の except
句を先頭に持ってくると、そこですべての例外がキャッチされてしまうので OverflowError
や ZeroDivisionError
の except
句には到達することがありません。これでは複数の except
句を書いた意味がなくなるので、 except
句は必ず、小さい例外(例外クラスの継承ツリーで枝葉の方にある例外)から先に書く必要があります。
例外オブジェクトのアトリビュートを利用する
例外もひとつのオブジェクトなので、自由にアトリビュートを付けることができます。
例えば、 Requests ライブラリ の例外には、リクエストオブジェクトを格納した request
という名前のアトリビュートが付けられています。利用者は except
句の中でそのアトリビュートに自由にアクセスすることができます。
import requests
try:
requests.get('https://www.yahoo.co.jp', timeout=0.001)
except requests.exceptions.ConnectTimeout as e:
# ライブラリ側で用意してくれている e.requst アトリビュートが利用できる
print('URL "{}" へのリクエストがタイムアウトしました。'.format(e.request.url))
規模の大きめのライブラリでは、独自の例外クラスを用意していて、アトリビュートにさまざまな情報を提供していることがあります。ライブラリの独自の例外クラスを取り扱う場合にはどんなアトリビュートがあるのかをチェックしておくとよいでしょう。
コンテキストマネージャを使う
with
文で利用できる Python のコンテキストマネージャは、特定のコードブロックに対して「コンテキスト」を提供できる仕組みです。さまざまな利用方法がありますが、代表的な使い方のひとつに「特定の例外処理パターンを再利用しやすくする」というものがあります。
サンプルとして、 CSV ファイルを読み込んで各行を返すリーダーオブジェクトを提供するコンテキストマネージャの例を見てみましょう。
from contextlib import contextmanager
import csv
@contextmanager
def read_csv(path):
'''CSV ファイルの読み込みを行うコンテキストマネージャ'''
try:
f = open(path)
reader = csv.reader(f)
yield reader
finally:
if 'f' in locals():
f.close()
# コンテキストマネージャを使って CSV ファイルを読み込む
with read_csv('sample1.csv') as reader:
print([row[0] for row in reader])
# 出力例:
# =>
# ['アレックス', 'マーティ', 'メルマン', 'グロリア']
with read_csv('sample2.csv') as reader:
print([row[0] for row in reader])
# 出力例:
# =>
# ['ポー', 'タイガー', 'ヘビ', 'カマキリ', 'ツル', 'モンキー']
read_csv()
を定義することで、 try
と finally
を使った例外処理のパターンをシンプルな with
文で再利用できるようになります。
コードの中でよく似た例外処理のパターンが繰り返し出てきた場合は、共通の例外処理パターンをコンテキストマネージャ化することによって、無用なコードの重複を減らすことができます。
...
ということで Python の例外システムを活用する上で押さえておきたいポイント集でした。