dorapon2000’s diary

忘備録的な。セキュリティとかネットワークすきです。

pythonで例外クラスを引数にとる例外処理デコレータ

pythonの例外処理のプラクティスとして、例外処理デコレータを作るというやり方を最近知りました。更に発展させて、デコレータがどの例外の例外処理をするか選びたいとも思ってググりましたが、見つからなかったので、記事に残します。

一般的な例外処理

try-exceptを使った一般的な例外処理です。

class Exception1(BaseException):
    pass


def may_raise_exception():
    raise Exception1


def myfunc():
    try:
        may_raise_exception()
    except Exception1 as e:
        print('Exception1 occured!')

if __name__ == '__main__':
    myfunc()
$ python basic_exception.py
Exception1 occured!

デコレータを使った例外処理

自分が最近学んだやり方です。

import functools

class Exception0(BaseException):
    pass

class Exception1(BaseException):
    pass

def exception1(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except Exception1 as e:
            print('Exception1 occured!')
    return wrapper

def may_raise_exception():
    # raise Exception0
    raise Exception1

@exception1
def myfunc():
    may_raise_exception()

if __name__ == '__main__':
    myfunc()

myfuncでException1を発生したとき、デコレーター内のtry-exceptで処理して例外を握りつぶします。myfuncでException0が発生したときは、普通に例外が表示されます。

デコレータを使うことでtry-exceptをmyfuncの中に書かずに済み、見た目がスッキリしました。複数のメソッドで同じ例外処理をする必要があるときもデコレータをつけるだけなので便利です。

例外クラスを引数に取る例外処理デコレータ

上記の例外処理デコレータだけでも便利ですが、もう少し汎用性をもたせて、デコレータの引数にどの例外を受け取れるか指定したいと思い、実装しました。

import functools

# Exception0 〜 Exception3までの定義

def reraise(*catch_exceptions, reraise_exception):
    def decorated(f):
        @functools.wraps(f)
        def wrapped(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except catch_exceptions as e:
                print('may do something')
                raise raise_exception
        return wrapped
    return decorated


def may_raise_exception():
    raise Exception0
    # raise Exception1
    # raise Exception2


@reraise(Exception1, Exception2, reraise_exception=Exception3)
def myfunc():
    may_raise_exception()


if __name__ == '__main__':
    myfunc()

reraiseデコレータ(正確にはデコレータを返すreraise関数)はキャッチする複数個の例外クラスとリレイズする1つの例外クラスを引数に取ります。引数catch_exceptionsは可変長引数(*)なので、その後ろの引数reraise_exceptionはキーワード引数として渡す必要があります。

myfuncの動作としては、Exception0が発生したとき、reraiseの引数では指定していないためキャッチせず、そのままException0が発生します。myfuncでException1か2が発生したとき、reraiseデコレータの中でキャッチし代わりにException3が投げられます。

今回の例では引数で受け取った例外をリレイズしますが、except句の中身を変更することでそこはよしなに変更できます。

ここまでのコードはgithubにもあげています。

github.com

感想

ググって出なかったので自分が記事にしましたが、例外クラスを引数にとる例外処理デコレータはさすがにやりすぎなのかもしれないです。 普通、try句に入れるのは例外が発生する可能性のある必要最低限のコードです。例外処理デコレータはその関数すべてをtryで囲っているようなものなので、気をつけて使わないと範囲が広すぎます。

# OK
def myfunc() {
    # 何かしらの処理
    try:
        may_raise_exception()
    except Exception1:
        print('Exception1 occured!')
    # 何かしらの処理
}

# OK
@exception1
def myfunc() {
    may_raise_exception()
}

# tryの範囲が広い
@exception1
def myfunc() {
    # 何かしらの処理
    may_raise_exception()
    # 何かしらの処理
}

参考