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


139

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

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

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

PythonのPEP 3134「Exception Chaining and Embedded Tracebacks」では、既存の例外の処理中に新しい例外が発生したことを示すために、Python 3.0で「チェーン」例外オブジェクトとして受け入れられた変更について説明しています。

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


例外はすでに完全に多態的です-それらはすべてExceptionのサブクラスです。あなたは何をしようとしているのですか?「別のメッセージ」は、トップレベルの例外ハンドラーでかなり簡単です。なぜクラスを変えるのですか?
S.Lott、2009年

質問で説明されているように(今、コメントをありがとう):キャッチした例外を装飾して、より多くの情報を伝達できるようにしますが、失うことはありません。トップレベルのハンドラーは遅すぎます。
bignose 2009

Python 2.xで必要な処理を実行できる私のCausedExceptionクラスをご覧ください。また、Python 3では、例外の原因として複数の元の例外を与えたい場合に役立ちます。多分それはあなたのニーズに合います。
Alfe


python-2の場合、@ DevinJeanpierreと同様のことを行いますが、新しい文字列メッセージを追加するだけです。- except Exception as e> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> この解決策は、別のSOの質問からのものです。これはきれいではありませんが機能的です。
Trevor Boyd Smith

回答:


197

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

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

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

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


ではPythonの2、(で説明したように、このユースケースは良い答えを持っていない現れイアンBickingネッドBatchelder)。残念。


4
Ian Bickingは私のソリューションを説明していませんか?こんなおもしろい返事をしてしまったことを後悔していますが、これが受け入れられたのはおかしいです。
Devin Jeanpierre

1
@bignoseあなたは正しいというだけでなく、「フロブニケート」の使用のために私のポイントを得ました:)
デビッドM.

5
例外の連鎖は実際にはデフォルトの動作ですが、実際には問題の反対であり、作業が必要な最初の例外を抑制します。PEP409を
dev / peps /

1
Python 2でこれをどのように達成しますか?
セロテープ

1
正常に動作するようです(python 2.7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e)
alex

37

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を必要としません。私はそれを行うことをお勧めしません。


Devin、そこにトレースバックへの参照を保存します。その参照を明示的に削除してはいけませんか?
アラファンギオン2009

2
何も保存せず、おそらく範囲外となるローカル変数としてトレースバックを残しました。はい、ありませんが、関数内ではなくグローバルスコープでそのような例外を発生させると、より大きな問題が発生します。あなたの不満がそれがグローバルスコープで実行される可能性があるということだけである場合、適切な解決策は、説明する必要があり、99%の使用に関連しない無関係なボイラープレートを追加することではなく、解決策を書き直してそのようなことをしないことです今のように、何も変わらないように見せながら、必要です。
Devin Jeanpierre

4
Arafangionは、Pythonドキュメントのsys.exc_info() @Devin に関する警告を参照している可能性があります。「トレースバックの戻り値を、例外を処理している関数のローカル変数に割り当てると、循環参照が発生します。」ただし、次の注記では、Python 2.2以降、サイクルをクリーンアップできるが、単にそれを回避する方が効率的であると述べています。
Don Kirkby、2012年

5
2つの賢明なpythonistaからPythonで例外を再発生させるさまざまな方法の詳細:Ian BickingNed Batchelder
Rodrigue

11

キャッチした例外を拡張する独自の例外タイプを作成できます。

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

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

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

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


1
私が説明したユースケースは、例外を処理するためのものではありません。具体的には、それを処理しないことですが、コールスタックのさらに上位で処理できるように、いくつかの追加情報(追加のクラスと新しいメッセージ)を追加します。
bignose 2009

2

また、多くの場合、発生したエラーの「ラッピング」が必要なこともあります。

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

ラッパーの作成を使用する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


1
元の例外に対して「ラッピング」が何をするか説明してください。コードの目的は何ですか?また、それによってどのような動作が可能になりますか?
アレクシス

@alexis-いくつかの例を追加しました。役立つことを願っています
Aaron_ab

-2

あなたのニーズに対する最も簡単な解決策はこれです:

try:
     upload(file_id)
except Exception as upload_error:
     error_msg = "Your upload failed! File: " + file_id
     raise RuntimeError(error_msg, upload_error)

このようにして、後でメッセージと、アップロード機能によってスローされた特定のエラーを出力できます。


1
それは例外オブジェクトをキャッチしてから捨てますので、いいえ、それは質問のニーズを満たしていません。質問では、既存の例外を保持し、その例外に含まれるすべての有用な情報を使用してスタックを伝播し続ける方法を尋ねます。
bignose
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.