标签归档:polymorphism

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

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

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

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

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

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

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

I’m writing a module and want to have a unified exception hierarchy for the exceptions that it can raise (e.g. inheriting from a FooError abstract class for all the foo module’s specific exceptions). This allows users of the module to catch those particular exceptions and handle them distinctly, if needed. But many of the exceptions raised from the module are raised because of some other exception; e.g. failing at some task because of an OSError on a file.

What I need is to “wrap” the exception caught such that it has a different type and message, so that information is available further up the propagation hierarchy by whatever catches the exception. But I don’t want to lose the existing type, message, and stack trace; that’s all useful information for someone trying to debug the problem. A top-level exception handler is no good, since I’m trying to decorate the exception before it makes its way further up the propagation stack, and the top-level handler is too late.

This is partly solved by deriving my module foo‘s specific exception types from the existing type (e.g. class FooPermissionError(OSError, FooError)), but that doesn’t make it any easier to wrap the existing exception instance in a new type, nor modify the message.

Python’s PEP 3134 “Exception Chaining and Embedded Tracebacks” discusses a change accepted in Python 3.0 for “chaining” exception objects, to indicate that a new exception was raised during the handling of an existing exception.

What I’m trying to do is related: I need it also working in earlier Python versions, and I need it not for chaining, but only for polymorphism. What is the right way to do this?


回答 0

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

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

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

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


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

Python 3 introduced exception chaining (as described in PEP 3134). This allows, when raising an exception, to cite an existing exception as the “cause”:

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

The caught exception (exc, a KeyError) thereby becomes part of (is the “cause of”) the new exception, a ValueError. The “cause” is available to whatever code catches the new exception.

By using this feature, the __cause__ attribute is set. The built-in exception handler also knows how to report the exception’s “cause” and “context” along with the traceback.


In Python 2, it appears this use case has no good answer (as described by Ian Bicking and Ned Batchelder). Bummer.


回答 1

您可以使用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。我不建议这样做。

You can use sys.exc_info() to get the traceback, and raise your new exception with said traceback (as the PEP mentions). If you want to preserve the old type and message, you can do so on the exception, but that’s only useful if whatever catches your exception looks for it.

For example

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

Of course, this is really not that useful. If it was, we wouldn’t need that PEP. I’d not recommend doing it.


回答 2

您可以创建自己的异常类型,扩展您捕获的任何异常

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

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

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

编辑:我发现这种分析方法可以引发您自己的异常并保留原始异常。没有漂亮的解决方案。

You could create your own exception type that extends whichever exception you’ve caught.

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

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

But most of the time, I think it would be simpler to catch the exception, handle it, and either raise the original exception (and preserve the traceback) or raise NewException(). If I were calling your code, and I received one of your custom exceptions, I’d expect that your code has already handled whatever exception you had to catch. Thus I don’t need to access it myself.

Edit: I found this analysis of ways to throw your own exception and keep the original exception. No pretty solutions.


回答 3

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

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

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

I also found that many times i need some “wrapping” to errors raised.

This included both in a function scope and sometimes wrap only some lines inside a function.

Created a wrapper to be used a decorator and context manager:


Implementation

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

Usage examples

decorator

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

when calling do method, don’t worry about IndexError, just MyError

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

context manager

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

inside do2, in the context manager, if IndexError is raised, it will be wrapped and raised MyError


回答 4

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

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

这样,您以后就可以打印消息以及上载功能引发的特定错误

The most straighforward solution to your needs should be this:

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

In this way you can later print your message and the specific error throwed by the upload function