Python中的“内部异常”(带回溯)?

“Inner exception” (with traceback) in Python?

我的背景是C,我最近刚开始用Python编程。当抛出一个异常时,我通常希望将它包装在另一个异常中,这样可以添加更多信息,同时仍然显示完整的堆栈跟踪。在C中这很容易,但是在Python中我该怎么做呢?

在C中,我会这样做:

1
2
3
4
5
6
7
8
try
{
  ProcessFile(filePath);
}
catch (Exception ex)
{
  throw new ApplicationException("Failed to process file" + filePath, ex);
}

在python中,我可以做类似的事情:

1
2
3
4
try:
  ProcessFile(filePath)
except Exception as e:
  raise Exception('Failed to process file ' + filePath, e)

…但这会丢失内部异常的回溯!

编辑:我想看到异常消息和两个堆栈跟踪,并将它们关联起来。也就是说,我想在输出中看到这里发生了异常X,然后在那里发生了异常Y——和在C中一样。这在python 2.6中是可能的吗?目前为止我能做的最好的(基于格伦·梅纳德的回答)是:

1
2
3
4
try:
  ProcessFile(filePath)
except Exception as e:
  raise Exception('Failed to process file' + filePath, e), None, sys.exc_info()[2]

这包括消息和两个回溯,但它不显示在回溯中发生的异常。


Python 3

在Python3中,可以执行以下操作:

1
2
3
4
5
6
try:
    raise MyExceptionToBeWrapped("I have twisted my ankle")

except MyExceptionToBeWrapped as e:

    raise MyWrapperException("I'm not in a good shape") from e

这将产生如下结果:

1
2
3
4
5
6
7
8
9
   Traceback (most recent call last):
   ...
   MyExceptionToBeWrapped: ("I have twisted my ankle")

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

   Traceback (most recent call last):
   ...
   MyWrapperException: ("I'm not in a good shape")


Python 2

这很简单;把回溯作为第三个要提出的参数传递。

1
2
3
4
5
6
7
import sys
class MyException(Exception): pass

try:
    raise TypeError("test")
except TypeError, e:
    raise MyException(), None, sys.exc_info()[2]

当捕获一个异常并重新引发另一个异常时,请始终执行此操作。


python 3有raisefrom条款,用于连锁例外。Glenn的答案对于Python2.7来说非常好,但它只使用原始异常的回溯,并丢弃错误消息和其他细节。下面是Python2.7中的一些示例,这些示例将当前作用域中的上下文信息添加到原始异常的错误消息中,但保持其他细节不变。

已知异常类型

1
2
3
4
5
6
7
8
try:
    sock_common = xmlrpclib.ServerProxy(rpc_url+'/common')
    self.user_id = sock_common.login(self.dbname, username, self.pwd)
except IOError:
    _, ex, traceback = sys.exc_info()
    message ="Connecting to '%s': %s." % (config['connection'],
                                           ex.strerror)
    raise IOError, (ex.errno, message), traceback

这种风格的raise语句将异常类型作为第一个表达式,将元组中的异常类构造函数参数作为第二个表达式,将回溯作为第三个表达式。如果运行的版本早于python 2.2,请参阅sys.exc_info()上的警告。

任何异常类型

这是另一个更通用的例子,如果您不知道代码可能必须捕获哪种异常的话。缺点是它会丢失异常类型并只引发一个运行时错误。您必须导入traceback模块。

1
2
3
4
5
except Exception:
    extype, ex, tb = sys.exc_info()
    formatted = traceback.format_exception_only(extype, ex)[-1]
    message ="Importing row %d, %s" % (rownum, formatted)
    raise RuntimeError, message, tb

修改消息

这是另一个选项,如果异常类型允许您向它添加上下文。您可以修改异常的消息,然后重新发出它。

1
2
3
4
5
6
7
8
import subprocess

try:
    final_args = ['lsx', '/home']
    s = subprocess.check_output(final_args)
except OSError as ex:
    ex.strerror += ' for command {}'.format(final_args)
    raise

生成以下堆栈跟踪:

1
2
3
4
5
6
7
8
9
10
Traceback (most recent call last):
  File"/mnt/data/don/workspace/scratch/scratch.py", line 5, in <module>
    s = subprocess.check_output(final_args)
  File"/usr/lib/python2.7/subprocess.py", line 566, in check_output
    process = Popen(stdout=PIPE, *popenargs, **kwargs)
  File"/usr/lib/python2.7/subprocess.py", line 710, in __init__
    errread, errwrite)
  File"/usr/lib/python2.7/subprocess.py", line 1327, in _execute_child
    raise child_exception
OSError: [Errno 2] No such file or directory for command ['lsx', '/home']

您可以看到它显示了调用check_output()的行,但异常消息现在包括命令行。


在Python 3。

1
raise Exception('Failed to process file ' + filePath).with_traceback(e.__traceback__)

或者简单地

1
2
except Exception:
    raise MyException()

它将传播MyException,但如果不处理,则打印这两个异常。

在Python 2。

1
raise Exception, 'Failed to process file ' + filePath, e

通过取消__context__属性,可以防止打印这两个异常。在这里,我编写了一个上下文管理器,使用它可以即时捕获和更改您的异常:(请参阅http://docs.python.org/3.1/library/stdtypes.html了解它们的工作方式)

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
31
32
33
34
35
36
37
38
39
40
41
try: # Wrap the whole program into the block that will kill __context__.

    class Catcher(Exception):
        '''This context manager reraises an exception under a different name.'''

        def __init__(self, name):
            super().__init__('Failed to process code in {!r}'.format(name))

        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_val, exc_tb):
            if exc_type is not None:
                self.__traceback__ = exc_tb
                raise self

    ...


    with Catcher('class definition'):
        class a:
            def spam(self):
                # not really pass, but you get the idea
                pass

            lut = [1,
                   3,
                   17,
                   [12,34],
                   5,
                   _spam]


        assert a().lut[-1] == a.spam

    ...


except Catcher as e:
    e.__context__ = None
    raise


我认为在python2.x中不能这样做,但是类似于此功能的东西是python3的一部分。来自PEP 3134:

In today's Python implementation, exceptions are composed of three
parts: the type, the value, and the traceback. The 'sys' module,
exposes the current exception in three parallel variables, exc_type,
exc_value, and exc_traceback, the sys.exc_info() function returns a
tuple of these three parts, and the 'raise' statement has a
three-argument form accepting these three parts. Manipulating
exceptions often requires passing these three things in parallel,
which can be tedious and error-prone. Additionally, the 'except'
statement can only provide access to the value, not the traceback.
Adding the 'traceback' attribute to exception values makes all
the exception information accessible from a single place.

与C比较:

Exceptions in C# contain a read-only 'InnerException' property that
may point to another exception. Its documentation [10] says that
"When an exception X is thrown as a direct result of a previous
exception Y, the InnerException property of X should contain a
reference to Y." This property is not set by the VM automatically;
rather, all exception constructors take an optional 'innerException'
argument to set it explicitly. The 'cause' attribute fulfills
the same purpose as InnerException, but this PEP proposes a new form
of 'raise' rather than extending the constructors of all exceptions.
C# also provides a GetBaseException method that jumps directly to
the end of the InnerException chain; this PEP proposes no analog.

还要注意,Java、Ruby和Perl 5也不支持这种类型的东西。再次引用:

As for other languages, Java and Ruby both discard the original
exception when another exception occurs in a 'catch'/'rescue' or
'finally'/'ensure' clause. Perl 5 lacks built-in structured
exception handling. For Perl 6, RFC number 88 [9] proposes an exception
mechanism that implicitly retains chained exceptions in an array
named @@.


您可以使用我的causedException类在python2.x中链接异常(即使在python3中,如果您想将多个捕获的异常作为引发新异常的原因,它也很有用)。也许它能帮助你。


假设:

  • 您需要一个适用于python2的解决方案(对于纯python3,请参见raise ... from解决方案)
  • 只想丰富错误消息,例如提供一些附加上下文
  • 需要完整的堆栈跟踪

您可以使用文档https://docs.python.org/3/tutorial/errors.html中的简单解决方案引发异常:

1
2
3
4
5
try:
    raise NameError('HiThere')
except NameError:
    print 'An exception flew by!' # print or log, provide details about context
    raise # reraise the original exception, keeping full stack trace

输出:

1
2
3
4
An exception flew by!
Traceback (most recent call last):
  File"<stdin>", line 2, in ?
NameError: HiThere

看起来关键部分是独立的简化的"raise"关键字。这将在except块中重新引发异常。


也许你可以抓到相关的信息然后把它传出去?我在想:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import traceback
import sys
import StringIO

class ApplicationError:
    def __init__(self, value, e):
        s = StringIO.StringIO()
        traceback.print_exc(file=s)
        self.value = (value, s.getvalue())

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

try:
    try:
        a = 1/0
    except Exception, e:
        raise ApplicationError("Failed to process file", e)
except Exception, e:
    print e

为了在python 2和3之间实现最大的兼容性,可以在six库中使用raise_from。https://six.readthedocs.io/six.raise-from.下面是您的示例(为了清晰起见,稍微修改了一下):

1
2
3
4
5
6
import six

try:
  ProcessFile(filePath)
except Exception as e:
  six.raise_from(IOError('Failed to process file ' + repr(filePath)), e)