关于python:向异常添加信息?

Adding information to an exception?

我希望实现这样的目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def foo():
   try:
       raise IOError('Stuff ')
   except:
       raise

def bar(arg1):
    try:
       foo()
    except Exception as e:
       e.message = e.message + 'happens at %s' % arg1
       raise

bar('arg1')
1
2
Traceback...
  IOError('Stuff Happens at arg1')

但我得到的是:

1
2
Traceback..
  IOError('Stuff')

关于如何实现这一点的任何线索? 如何在Python 2和3中完成它?


我这样做,所以改变foo()中的类型不需要在bar()中更改它。

1
2
3
4
5
6
7
8
9
10
11
12
13
def foo():
    try:
        raise IOError('Stuff')
    except:
        raise

def bar(arg1):
    try:
        foo()
    except Exception as e:
        raise type(e)(e.message + ' happens at %s' % arg1)

bar('arg1')
1
2
3
4
5
6
Traceback (most recent call last):
  File"test.py", line 13, in <module>
    bar('arg1')
  File"test.py", line 11, in bar
    raise type(e)(e.message + ' happens at %s' % arg1)
IOError: Stuff happens at arg1

更新1

这是一个保留原始追溯的略微修改:

1
2
3
4
5
6
7
8
9
10
...
def bar(arg1):
    try:
        foo()
    except Exception as e:
        import sys
        raise type(e), type(e)(e.message +
                               ' happens at %s' % arg1), sys.exc_info()[2]

bar('arg1')
1
2
3
4
5
6
7
8
Traceback (most recent call last):
  File"test.py", line 16, in <module>
    bar('arg1')
  File"test.py", line 11, in bar
    foo()
  File"test.py", line 5, in foo
    raise IOError('Stuff')
IOError: Stuff happens at arg1

更新2

对于Python 3.x,我的第一次更新中的代码在语法上是不正确的,加上在BaseException上有一个message属性的想法在2012-05-16更改为PEP 352时缩回(我的第一个更新发布于2012-03-12)。所以目前,无论如何,在Python 3.5.2中,您需要沿着这些行做一些事情以保留回溯,而不是在函数bar()中硬编码异常类型。另请注意,会有一行:

1
During handling of the above exception, another exception occurred:

在显示的追溯消息中。

1
2
3
4
5
6
7
8
9
10
11
# for Python 3.x
...
def bar(arg1):
    try:
        foo()
    except Exception as e:
        import sys
        raise type(e)(str(e) +
                      ' happens at %s' % arg1).with_traceback(sys.exc_info()[2])

bar('arg1')

更新3

一位评论者询问是否有一种方法可以在Python 2和3中运行。虽然由于语法差异,答案似乎是"否",但通过使用像reraise()这样的辅助函数,可以解决这个问题。 six附加模块。因此,如果您因某些原因而不想使用该库,则下面是一个简化的独立版本。

另请注意,由于异常在reraise()函数内重新启动,因此会出现在引发的任何回溯中,但最终结果是您想要的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import sys

if sys.version_info.major < 3:  # Python 2?
    # Using exec avoids a SyntaxError in Python 3.
    exec("""def reraise(exc_type, exc_value, exc_traceback=None):
                raise exc_type, exc_value, exc_traceback"""
)
else:
    def reraise(exc_type, exc_value, exc_traceback=None):
        if exc_value is None:
            exc_value = exc_type()
        if exc_value.__traceback__ is not exc_traceback:
            raise exc_value.with_traceback(exc_traceback)
        raise exc_value

def foo():
    try:
        raise IOError('Stuff')
    except:
        raise

def bar(arg1):
    try:
       foo()
    except Exception as e:
        reraise(type(e), type(e)(str(e) +
                                 ' happens at %s' % arg1), sys.exc_info()[2])

bar('arg1')


如果您来到这里寻找Python 3的解决方案,手册说:

When raising a new exception (rather than using a bare raise to re-raise the exception currently being handled), the implicit exception context can be supplemented with an explicit cause by using from with raise:

1
raise new_exc from original_exc

例:

1
2
3
4
5
6
try:
    return [permission() for permission in self.permission_classes]
except TypeError as e:
    raise TypeError("Make sure your view's 'permission_classes' are iterable."
                   "If you use '()' to generate a set with a single element"
                   "make sure that there is a comma behind the one (element,).") from e

最终看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
2017-09-06 16:50:14,797 [ERROR] django.request: Internal Server Error: /v1/sendEmail/
Traceback (most recent call last):
File"venv/lib/python3.4/site-packages/rest_framework/views.py", line 275, in get_permissions
    return [permission() for permission in self.permission_classes]
TypeError: 'type' object is not iterable

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
    # Traceback removed...
TypeError: Make sure your view's Permission_classes are iterable. If
     you use parens () to generate a set with a single element make
     sure that there is a (comma,) behind the one element.

将一个完全不起眼的TypeError转换成一条带有提示解决方案的好消息,而不会弄乱原始的异常。


假设您不想或不能修改foo(),您可以这样做:

1
2
3
4
5
6
try:
    raise IOError('stuff')
except Exception as e:
    if len(e.args) >= 1:
        e.args = (e.args[0] + ' happens',) + e.args[1:]
    raise

这确实是解决Python 3中的问题的唯一解决方案,没有丑陋和令人困惑的"在处理上述异常时,发生了另一个异常"消息。

如果应该将重新提升行添加到堆栈跟踪中,则写入raise e而不是raise将会起作用。


我使用的一个方便的方法是使用class属性作为存储细节,
因为class属性可以从类对象和类实例访问:

1
2
class CustomError(Exception):
    details = None

然后在你的代码中:

1
2
3
exc = CustomError('Some message')
exc.details('Details -- add whatever you want')
raise exc

并在捕获错误时:

1
2
3
4
except CustomError, e:
    # Do whatever you want with the exception instance
    print e
    print e.details


我将提供一段代码,每当我想要向异常添加额外信息时,我经常使用这些代码。我在Python 2.7和3.6中都工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import sys
import traceback

try:
    a = 1
    b = 1j

    # The line below raises an exception because
    # we cannot compare int to complex.
    m = max(a, b)  

except Exception as ex:
    # I create my  informational message for debugging:
    msg ="a=%r, b=%r" % (a, b)

    # Gather the information from the original exception:
    exc_type, exc_value, exc_traceback = sys.exc_info()

    # Format the original exception for a nice printout:
    traceback_string = ''.join(traceback.format_exception(
        exc_type, exc_value, exc_traceback))

    # Re-raise a new exception of the same class as the original one,
    # using my custom message and the original traceback:
    raise type(ex)("%s

ORIGINAL TRACEBACK:

%s
"
% (msg, traceback_string))

上面的代码产生以下输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-6-09b74752c60d> in <module>()
     14     raise type(ex)(
     15        "%s

ORIGINAL TRACEBACK:

%s
"
%
---> 16         (msg, traceback_string))

TypeError: a=1, b=1j

ORIGINAL TRACEBACK:

Traceback (most recent call last):
  File"<ipython-input-6-09b74752c60d>", line 7, in <module>
    m = max(a, b)  # Cannot compare int to complex
TypeError: no ordering relation is defined for complex numbers

我知道这与问题中提供的示例略有不同,但我希望有人发现它有用。


与以前的答案不同,这适用于非常糟糕的__str__的异常。
但它会修改类型,以便分解无用的__str__实现。

我仍然希望找到一个不会修改类型的额外改进。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from contextlib import contextmanager
@contextmanager
def helpful_info():
    try:
        yield
    except Exception as e:
        class CloneException(Exception): pass
        CloneException.__name__ = type(e).__name__
        CloneException.__module___ = type(e).__module__
        helpful_message = '%s

helpful info!'
% e
        import sys
        raise CloneException, helpful_message, sys.exc_traceback


class BadException(Exception):
    def __str__(self):
        return 'wat.'

with helpful_info():
    raise BadException('fooooo')

保留原始回溯和类型(名称)。

1
2
3
4
5
6
7
8
9
10
11
12
Traceback (most recent call last):
  File"re_raise.py", line 20, in <module>
    raise BadException('fooooo')
  File"/usr/lib64/python2.6/contextlib.py", line 34, in __exit__
    self.gen.throw(type, value, traceback)
  File"re_raise.py", line 5, in helpful_info
    yield
  File"re_raise.py", line 20, in <module>
    raise BadException('fooooo')
__main__.BadException: wat.

helpful info!


您可以定义从另一个继承的自己的异常,并创建它自己的构造函数来设置值。

例如:

1
2
3
4
5
6
7
class MyError(Exception):
   def __init__(self, value):
     self.value = value
     Exception.__init__(self)

   def __str__(self):
     return repr(self.value)


也许

1
2
except Exception as e:
    raise IOError(e.message + 'happens at %s'%arg1)