2018/05/23

Python Tips: Python の例外システムを活用したい

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)

キーワード tryexcept を組み合わせて使います。

他の言語を知る方だと try catch のパターンに馴染みのある方が多いでしょうか。 Python の場合は catch の代わりに except を使用します。

except 句は一般に次のいずれかのパターンで書きます。

except [キャッチしたい例外のクラス]:  
except [キャッチしたい例外のクラス] as [キャッチされた例外オブジェクトにつける名前]: 

finally を押さえる


tryexcept の後には 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 句を抜ける場合 → returnbreak の後に実行される:
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 句は要らないんじゃないか」と思えますが(私は思いました)、 elsetry の中身を最小限に留め思わぬ例外処理が起こってしまうのを防ぐのに有効です。 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 句を複数連ねるときの注意点は、例外の場合分けを適切にするためには「 小さい例外を前に、大きな例外を後に書かなくてはいけない 」ことです。

上のサンプルではこのとおりに「小さい例外を先、大きな例外を後」にして書いていますが、これを変えて Exceptionexcept 句を先頭に持ってくると、そこですべての例外がキャッチされてしまうので OverflowErrorZeroDivisionErrorexcept 句には到達することがありません。これでは複数の except 句を書いた意味がなくなるので、 except 句は必ず、小さい例外(例外クラスの継承ツリーで枝葉の方にある例外)から先に書く必要があります。

例外オブジェクトのアトリビュートを利用する


例外もひとつのオブジェクトなので、自由にアトリビュートを付けることができます。

例えば、 Requests ライブラリ の例外には、リクエストオブジェクトを格納した request という名前のアトリビュートが付けられています。利用者は except 句の中でそのアトリビュートに自由にアクセスすることができます。

import requests


try:
    requests.get('http://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() を定義することで、 tryfinally を使った例外処理のパターンをシンプルな with 文で再利用できるようになります。

コードの中でよく似た例外処理のパターンが繰り返し出てきた場合は、共通の例外処理パターンをコンテキストマネージャ化することによって、無用なコードの重複を減らすことができます。

...

以上、 Python の例外システムを活用する上で押さえておきたいポイント集でした。

以上のことがひととおりきちんと押さえられれば、 Python の例外処理については「正しく使えている」と自信を持ってよいのではないでしょうか。


参考

0 件のコメント: