使用不同的类型和消息重新引发异常,保留现有信息

Posted

技术标签:

【中文标题】使用不同的类型和消息重新引发异常,保留现有信息【英文标题】:Re-raise exception with a different type and message, preserving existing information 【发布时间】:2010-10-16 07:30:48 【问题描述】:

我正在编写一个模块,并希望为它可以引发的异常有一个统一的异常层次结构(例如,从 FooError 抽象类继承所有 foo 模块的特定异常)。这允许模块的用户捕获那些特定的异常并在需要时清楚地处理它们。但是从模块中引发的许多异常都是由于其他一些异常而引发的;例如由于文件上的 OSError,某些任务失败。

我需要“包装”捕获的异常,使其具有不同的类型和消息,以便通过捕获异常的任何内容,可以在传播层次结构中进一步获取信息。但我不想丢失现有的类型、消息和堆栈跟踪;对于试图调试问题的人来说,这些都是有用的信息。***异常处理程序不好,因为我试图在异常进入传播堆栈之前对其进行修饰,而***处理程序为时已晚。

通过从现有类型(例如class FooPermissionError(OSError, FooError))派生我的模块foo 的特定异常类型,部分解决了这个问题,但这并没有使将现有异常实例包装在新类型中变得更容易,也不修改消息。

Python 的 PEP 3134“异常链接和嵌入式回溯”讨论了 Python 3.0 中接受的“链接”异常对象的更改,以表明在处理现有异常期间引发了新异常。

我正在尝试做的是相关的:我需要它在早期的 Python 版本中也能工作,而且我需要它不是用于链接,而只是用于多态性。这样做的正确方法是什么?

【问题讨论】:

异常已经是完全多态的——它们都是异常的子类。你想做什么? “不同的消息”对于***异常处理程序来说是相当微不足道的。你为什么要换班? 正如问题中所解释的(现在,感谢您的评论):我正在尝试修饰我捕获的异常,以便它可以进一步传播更多信息但不会丢失任何信息。***处理程序为时已晚。 请看我的CausedException class,它可以在 Python 2.x 中做你想做的事。同样在 Python 3 中,如果您想给出多个原始异常作为异常原因,它也很有用。也许它符合您的需求。 bignose has the python-3 solution。 @DevinJeanpierre has the python-2 solution (kind of). 对于 python-2 我做了类似于@DevinJeanpierre 但我只是附加一个新的字符串消息:except Exception as e --> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2] --> this solution is from another SO question。这不漂亮但实用。 【参考方案1】:

Python 3 引入了异常链(如PEP 3134 中所述)。这允许在引发异常时引用现有异常作为“原因”:

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

捕获的异常(exc,一个 KeyError)因此成为新异常 ValueError 的一部分(是“原因”)。 “原因”可用于捕获新异常的任何代码。

通过使用此功能,设置了__cause__ 属性。内置的异常处理程序还 knows how to report the exception's “cause” and “context” 以及回溯。


Python 2 中,这个用例似乎没有好的答案(如 Ian Bicking 和 Ned Batchelder 所述)。无赖。

【讨论】:

Ian Bicking 没有描述我的解决方案吗?我很遗憾我给出了如此糟糕的答案,但这个答案被接受了很奇怪。 @bignose 你明白了我的意思,不仅是因为正确,而且是因为“frobnicate”的使用:) 异常链接实际上是现在的默认行为,实际上它是相反的问题,抑制需要工作的第一个异常,请参阅 PEP 409 python.org/dev/peps/pep-0409 你将如何在 python 2 中完成这个? 它似乎工作正常(python 2.7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e)【参考方案2】:

您可以使用 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,您在此处存储了对回溯的引用,您不应该明确删除该引用吗? 我没有存储任何东西,我将回溯作为可能超出范围的局部变量留下。是的,可以想象它不会,但是如果你在全局范围内而不是在函数内引发类似的异常,你就会遇到更大的问题。如果您的抱怨只是它可以在全球范围内执行,那么正确的解决方案不是添加必须解释且与 99% 的用途无关的不相关样板,而是重写解决方案,以便没有这样的事情是必要的,同时让它看起来好像没有什么不同——就像我现在所做的那样。 Arafangion 可能指的是Python documentation for sys.exc_info()、@Devin 中的警告。它说,“将回溯返回值分配给正在处理异常的函数中的局部变量将导致循环引用。”但是,下面的注释说,从 Python 2.2 开始,可以清理循环,但避免它更有效。 更多关于在 Python 中从两个开明的 pythonistas 中重新引发异常的不同方法的详细信息:Ian Bicking 和 Ned Batchelder【参考方案3】:

您可以创建自己的异常类型来扩展您捕获的whichever exception。

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

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

但大多数时候,我认为捕获异常、处理它以及raise 原始异常(并保留回溯)或raise NewException() 会更简单。如果我正在调用您的代码,并且收到您的自定义异常之一,我希望您的代码已经处理了您必须捕获的任何异常。因此我不需要自己访问它。

编辑:我找到了this analysis 的方法来抛出自己的异常并保留原始异常。没有漂亮的解决方案。

【讨论】:

我描述的用例不是为了处理异常;它专门关于处理它,而是添加一些额外的信息(一个额外的类和一个新消息),以便可以在调用堆栈中进一步处理它。【参考方案4】:

我还发现很多时候我需要对所引发的错误进行一些“包装”。

这包括在函数范围内,有时只在函数内包装一些行。

创建了一个用于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

【讨论】:

请解释“包装”将对原始异常做什么。您的代码的目的是什么,它支持什么行为? @alexis - 添加了一些示例,希望对您有所帮助【参考方案5】:

满足您需求的最直接的解决方案应该是:

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

这样你以后可以打印你的消息和上传函数抛出的具体错误

【讨论】:

捕获然后丢弃异常对象,所以不,它不符合问题的需要。该问题询问如何保留现有异常并允许同一异常及其包含的所有有用信息继续向上传播堆栈。

以上是关于使用不同的类型和消息重新引发异常,保留现有信息的主要内容,如果未能解决你的问题,请参考以下文章

重新传递 Mule ESB 处理的失败的 activemq jms 消息时保留异常原因

您可以在不同的线程上重新引发 .NET 异常吗?

在 Ruby 中捕获异常后重新引发(相同的异常)

如何在 SQL Server 中重新引发相同的异常

SimpleMessageListenerContainer:消费者引发异常,如果处理支持它可以重新启动处理

SQL Server:使用原始异常号重新引发异常