用其他类型和消息重新引发异常,保留现有信息


139

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

我需要的是“包装”捕获的异常,使其具有不同的类型和消息,以便通过捕获异常的方式在传播层次结构中进一步获取信息。但是我不想丢失现有的类型,消息和堆栈跟踪;这对于尝试调试问题的人来说都是有用的信息。顶级异常处理程序是不好的,因为我正在尝试在异常传播到传播堆栈之前对其进行装饰,并且顶级处理程序为时已晚。

这可以通过foo从现有类型(例如class FooPermissionError(OSError, FooError))中派生模块的特定异常类型来部分解决,但这并没有使将现有异常实例包装为新类型或修改消息变得更加容易。

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

我想做的是相关的:我需要它在早期的Python版本中也能工作,我不需要链,而只需要多态。什么是正确的方法?


异常已经完全是多态的-它们都是Exception的子类。你想做什么?对于顶级异常处理程序,“不同的消息”是相当琐碎的。你为什么要换班?
S.Lott

正如问题中所解释的(现在,感谢您的评论):我正在尝试装饰一个捕获的异常,以便它可以传播更多的信息而又不会丢失任何信息。顶级处理程序为时已晚。
bignose

请查看我的CausedException类该类可以在Python 2.x中完成您想要的操作。同样在Python 3中,如果您想给出一个以上的原始异常作为异常原因,也可以使用它。也许它符合您的需求。
Alfe 2012年


对于python-2,我执行类似于@DevinJeanpierre的操作,但我只是附加了一个新的字符串消息:except Exception as e-> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> 此解决方案来自另一个SO问题。这不是很漂亮,但是很实用。
Trevor Boyd Smith,

Answers:


197

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

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

因此,捕获的异常成为新异常的一部分(是“原因”),并且任何捕获新异常的代码均可使用。

通过使用此功能,__cause__可以设置属性。内置的异常处理程序还知道如何报告异常的“原因”和“上下文”以及回溯。


Python 2中,该用例似乎没有很好的答案(如Ian BickingNed Batchelder所述)。笨蛋


4
Ian Bicking不能描述我的解决方案吗?很遗憾我给出了如此惊人的答案,但是这个答案被接受很奇怪。
Devin Jeanpierre 2011年

1
@bignose您不仅从正确的角度理解了我的观点,而且还因为使用了“ frobnicate” :)
David M.

5
异常链接实际上是现在的默认行为,实际上这是相反的问题,抑制了需要工作的第一个异常,请参阅PEP 409 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 17'Sep

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,您在其中存储了对追溯的引用,您是否应该明确删除该引用?
Arafangion

2
我什么都没存储,我将traceback保留为局部变量,该变量可能超出范围。是的,可以想象没有。但是,如果在全局范围而不是函数内部引发类似的异常,则会遇到更大的问题。如果您仅抱怨可以在全局范围内执行,则正确的解决方案不是添加必须说明且与99%的使用都不相关的无关的样板,而是重写解决方案,以免发生此类情况就像我现在所做的那样,在使它看起来没有什么不同的同时也很有必要。
Devin Jeanpierre,2009年

4
Arafangion可能是在Python文档中针对sys.exc_info() @Devin的警告。它说:“在处理异常的函数中将回溯返回值分配给局部变量将导致循环引用。” 但是,以下说明说,从Python 2.2开始,可以清除该循环,但是避免它更有效。
唐·柯比

5
有关从两个开明的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
我描述的用例不是用于处理异常的;这是专门针对处理它,而是添加一些附加信息(附加类和新消息),以便可以在调用堆栈中进一步处理它。
bignose

2

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

这既包含在函数范围内,有时也仅在函数内包含一些行。

创建了要用于decorator和的包装器context 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方法时,不必担心IndexErrorMyError

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-添加了一些示例,希望对您
有所

-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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.