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

201

我正在编写一个模块,希望为它可能引发的异常(例如从FooError抽象类继承所有Foo模块的特定异常)建立统一异常层次结构。这使得模块的用户可以捕获这些特定的异常并进行不同的处理(如果需要)。但是,该模块引发的许多异常是由于其他异常引起的;例如,在文件上有OSError时,执行某些任务失败。

我需要的是“包装”被捕获的异常,以便它具有不同的类型和消息,以便任何捕获异常的人都能够在更高的传播层次结构中获得信息。但我不想失去现有的类型、消息和堆栈跟踪;对于尝试调试问题的人来说,这都是有用的信息。顶级异常处理程序无法胜任此工作,因为我正在尝试在异常到达更高的传播堆栈之前修饰异常,而顶级处理程序太晚了。

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

Python的PEP 3134“异常链接和嵌入式跟踪”讨论了Python 3.0中接受的一项变更,用于“链接”异常对象,以指示在处理现有异常期间引发了新异常。

我正在尝试做的与此相关:我需要让它在早期版本的Python中工作,并且我只需要它用于多态性,而不是用于链接。应该如何正确地实现这个功能?


异常已完全具有多态性——它们都是Exception的子类。你想要做什么?通过顶层异常处理程序,"不同的信息"相当容易。为什么要更改类呢? - S.Lott
1
正如问题所解释的(现在,感谢您的评论):我正在尝试修饰我捕获的异常,以便它可以向上传递更多信息,但不会丢失任何信息。顶级处理程序太晚了。 - bignose
请查看我的 CausedException 类,它可以在 Python 2.x 中实现您想要的功能。同时,如果您想将多个原始异常作为异常的原因,它在 Python 3 中也很有用。也许它符合您的需求。 - Alfe
显示剩余2条评论
7个回答

318
Python 3引入了异常链(如PEP 3134所述)。这允许在引发异常时引用现有异常作为“原因”:
try:
    frobnicate()
except KeyError as exc:
    raise ValueError("Bad grape") from exc

捕获的异常(exc,一个KeyError)因此成为新异常(ValueError)的一部分,“原因”可供捕获新异常的任何代码使用。
通过使用此功能,设置了__cause__属性。内置异常处理程序还知道如何报告异常的“原因”和“上下文”以及回溯信息。
在Python 2中,似乎没有很好的答案来解决这个用例(正如Ian BickingNed Batchelder所描述的那样)。遗憾。

4
伊恩·比金是否描述了我的解决方案?我很遗憾地回答得如此糟糕,但奇怪的是这个答案竟然被接受了。 - Devin Jeanpierre
1
@bignose 你不仅因为正确,而且因为使用了“frobnicate”这个词理解了我的观点 :) - David M.
7
现在的默认行为实际上是异常链式传递,事实上相反的行为才是问题,会抑制第一个需要处理的异常,详情请参阅 PEP 409 https://www.python.org/dev/peps/pep-0409/。 - Chris_Rands
1
你会如何在Python 2中完成这个任务? - selotape
1
看起来运行得很好(Python 2.7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e) - alex
显示剩余2条评论

40
你可以使用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,你在那里存储了一个回溯的引用,难道你不应该明确地删除那个引用吗? - Arafangion
2
我没有存储任何东西,我将traceback作为本地变量留下来,这个变量可能会超出范围。是的,它有可能不会超出范围,但如果你在全局范围而不是函数内部引发异常,那么你就有更大的问题了。如果你的抱怨只是它可以在全局范围内执行,正确的解决方案不是添加无关的样板代码,必须进行解释,并且对99%的用途都不相关,而是重新编写解决方案,使之不需要这样做,同时使其看起来好像没有任何不同--正如我现在所做的。 - Devin Jeanpierre
4
Arafangion 可能是指 Python 文档中关于 sys.exc_info() 的警告,@Devin。文档中写道:"在处理异常的函数中将 traceback 返回值分配给本地变量会导致循环引用。" 然而,随后的注释指出自从 Python 2.2 开始,这个循环引用可以被清除,但最好还是避免它以提高效率。 - Don Kirkby
5
两位Python专家Ian BickingNed Batchelder提供了关于在Python中不同方式重新引发异常的更多细节。请注意,翻译过程中保留原文意思,使内容通俗易懂,不得添加解释或其他信息。 - Rodrigue

16
您可以创建自己的异常类型,该类型扩展您捕获的任何异常。whichever exception
class NewException(CaughtException):
    def __init__(self, caught):
        self.caught = caught

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

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

编辑:我找到了这篇分析文章,介绍了抛出自定义异常并保留原始异常的几种方法。没有很好看的解决方案。


2
我描述的用例并不是为了处理异常;它特别是关于处理它,而是添加一些额外的信息(一个附加类和一个新消息),以便可以在调用堆栈上进一步处理。 - bignose
博客文章(本文分析)的网址已更改。现在位于:https://ianbicking.org/blog/2007/09/re-raising-exceptions.html - RagingRoosevelt

6

我发现很多时候需要对抛出的错误进行“包装”。

这既包括在函数范围内,有时也只是包装函数内的某些行。

我创建了一个可用作装饰器上下文管理器的包装器:


实现方式

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 内,在 上下文管理器 中,如果引发了 IndexError,它将被包装并引发 MyError


1
请解释一下"wrapping"对原始异常的影响。你的代码的目的是什么,它能实现什么行为? - alexis
@alexis - 添加了一些示例,希望能有所帮助。 - Aaron_ab

1

这个话题有些离题,但是在为我的自己的库构建一致的错误信息时,我发现我们正在将自己的错误消息包装起来。很高兴看到Python 3.11现在提供了一个add_note函数,可以用来增加额外的信息来增强现有的错误,这也可能非常有用。

对于Python 3.11 add_note()

add_note()方法被添加到BaseException中。它可以用于丰富异常的上下文信息,在引发异常时不可用。添加的注释出现在默认的回溯中。

所以我们现在遵循这个模式:

try:
   some_risky_business()
except MyCustomException as ce:
  ce.add_note(f"Here is some more critical context!")
  raise se
except Exception as e:
  raise MyCustomException("Wow, didn't expect this error.") from e

补充一下,或者完全替换消息都很容易 - 在Python 3.x中,异常有一个args属性 - 参见https://dev59.com/qnI-5IYBdhLWcg3wBjdR#24065533 - darda

0

为了真正“转换”异常并避免像@bignose的答案所解释的上下文或原因,您必须执行一些奇怪的操作(以下是Python 3):

import sys

new_ex = None

try:
    something_that_raises_ValueError()
except ValueError:
    _, _, tb = sys.exc_info()
    new_ex = TypeError('This is really how I want to report this')

if new_ex is not None:
    raise new_ex.with_traceback(tb)

通过传递回溯信息,您可以使其指向问题发生的位置,而不是您的raise语句。

这可能可以转换为上下文以使其更具可重用性。

请注意,如果您只想更改消息,可以操作args。我有这两个函数:

def append_message(e_: Exception, msg: str):
    """
    Appends `msg` to the message text of the exception `e`.

    Parameters
    ----------
    e_: Exception
        An exception instance whose `args` will be modified to include `msg`.

    msg: str
        The string to append to the message.
    """
    if e_.args:
        # Exception was raised with arguments
        e_.args = (str(e_.args[0]) + msg,) + e_.args[1:]
    else:
        e_.args = (msg,)


def replace_message(e_: Exception, msg: str):
    """
    Replaces the exception message with `msg`.

    Parameters
    ----------
    e_: Exception
        An exception instance whose `args` will be modified to be `msg`.

    msg: str
        The string to replace the exception message with.
    """
    if e_.args:
        e_.args = (msg,) + e_.args[1:]
    else:
        e_.args = (msg,)

-3

对于您的需求,最直接的解决方案应该是这样的:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接