web-dev-qa-db-ja.com

異なるタイプとメッセージで例外を再発生し、既存の情報を保持します

私はモジュールを書いていますが、発生する可能性のある例外(たとえば、すべてのFooErrorモジュールの特定の例外のfoo抽象クラスから継承する)の統一された例外階層を持ちたいと思っています。これにより、モジュールのユーザーはこれらの特定の例外をキャッチし、必要に応じて明確に処理できます。ただし、モジュールから発生する例外の多くは、他の例外のために発生します。例えばファイルのOSErrorが原因で、あるタスクで失敗します。

必要なのは、キャッチされた例外を「ラップ」して、タイプとメッセージが異なるようにすることです。例外をキャッチするものは何でも。しかし、既存のタイプ、メッセージ、スタックトレースを失いたくありません。これは、問題をデバッグしようとしている人にとって有益な情報です。最上位の例外ハンドラーは、伝播スタックをさらに上に移動する前に例外を装飾しようとしており、最上位のハンドラーが遅すぎるため、良くありません。

これは、モジュールfooの特定の例外タイプを既存のタイプ(たとえば、class FooPermissionError(OSError, FooError))から派生させることで部分的に解決されますが、既存の例外インスタンスを新しいものにラップするのは簡単ではありませんメッセージを入力または変更しません。

Pythonの PEP 3134 「Exception Chaining and Embedded Tracebacks」では、Python 3.0で受け入れられた変更について説明し、新しい例外が既存の例外の処理。

私がやろうとしていることは関連しています:以前のPythonバージョンでも動作する必要があり、チェーンのためではなく、ポリモーフィズムのためだけに必要です。これを行う正しい方法は何ですか? ?

116
bignose

Python 3導入例外連鎖PEP 3134 )。これにより、例外を発生させるときに、既存の例外を「原因」として引用できます。

try:
    frobnicate()
except KeyError as exc:
    raise ValueError("Bad grape") from exc

これにより、キャッチされた例外は、新しい例外の一部(「原因」)になり、新しい例外をキャッチするすべてのコードで使用できます。

この機能を使用すると、__cause__属性が設定されています。組み込みの例外ハンドラーも 例外の「原因」と「コンテキスト」を報告する方法を知っています トレースバックとともに。


Python 2では、このユースケースには良い答えがないと思われます( Ian Bicking および Ned Batchelder )。残念。

160
bignose

Sys.exc_info()を使用してトレースバックを取得し、トレースバックを使用して新しい例外を発生させることができます(PEPの言及どおり)。古いタイプとメッセージを保持したい場合は、例外でそれを行うことができますが、それは例外をキャッチするものがそれを探す場合にのみ有用です。

例えば

import sys

def failure():
    try: 1/0
    except ZeroDivisionError, e:
        type, value, traceback = sys.exc_info()
        raise ValueError, ("You did something wrong!", type, value), traceback

もちろん、これは実際にはそれほど便利ではありません。そうであれば、そのPEPは必要ありません。私はそれをすることをお勧めしません。

35

いずれの例外 キャッチした独自の例外タイプを作成できます。

_class NewException(CaughtException):
    def __init__(self, caught):
        self.caught = caught

try:
    ...
except CaughtException as e:
    ...
    raise NewException(e)
_

しかし、ほとんどの場合、例外をキャッチして処理し、raise元の例外(およびトレースバックを保持)またはraise NewException()のいずれかを使用する方が簡単だと思います。私があなたのコードを呼び出していて、カスタム例外の1つを受け取った場合、あなたのコードはあなたがキャッチしなければならない例外をすでに処理していると思います。したがって、自分でアクセスする必要はありません。

編集:私は この分析 独自の例外をスローし、元の例外を保持する方法を見つけました。きれいな解決策はありません。

11
Nikhil Chelliah

また、発生したエラーに「ラップ」する必要があることも何度もありました。

これには、関数スコープに含まれている場合と、関数内の一部の行のみをラップする場合があります。

decoratorcontext managerを使用するラッパーを作成しました。


実装

import inspect
from contextlib import contextmanager, ContextDecorator
import functools    

class wrap_exceptions(ContextDecorator):
    def __init__(self, wrapper_exc, *wrapped_exc):
        self.wrapper_exc = wrapper_exc
        self.wrapped_exc = wrapped_exc

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        if not exc_type:
            return
        try:
            raise exc_val
        except self.wrapped_exc:
            raise self.wrapper_exc from exc_val

    def __gen_wrapper(self, f, *args, **kwargs):
        with self:
            for res in f(*args, **kwargs):
                yield res

    def __call__(self, f):
        @functools.wraps(f)
        def wrapper(*args, **kw):
            with self:
                if inspect.isgeneratorfunction(f):
                    return self.__gen_wrapper(f, *args, **kw)
                else:
                    return f(*args, **kw)
        return wrapper

使用例

デコレータ

@wrap_exceptions(MyError, IndexError)
def do():
   pass

doメソッドを呼び出すときは、IndexErrorを心配する必要はありません。ただMyError

try:
   do()
except MyError as my_err:
   pass # handle error 

コンテキストマネージャー

def do2():
   print('do2')
   with wrap_exceptions(MyError, IndexError):
       do()

do2内、context manager内で、IndexErrorが発生した場合、ラップされて発生しますMyError

2
Aaron_ab