Python中的“内部异常”(带有回溯信息)是什么?

175

我曾经主要使用C#编程,最近才开始学习Python。当出现异常时,我通常会将其包装在另一个异常中,以添加更多信息,同时仍然显示完整的堆栈跟踪。在C#中很容易实现,但在Python中该如何处理呢?

例如,在C#中,我会这样做:

try
{
  ProcessFile(filePath);
}
catch (Exception ex)
{
  throw new ApplicationException("Failed to process file " + filePath, ex);
}

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

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

...但这会导致内部异常的回溯信息丢失!

编辑:我想看到两个异常消息和两个堆栈跟踪,然后将它们关联起来。也就是说,我希望在输出中看到异常X发生在这里,然后异常Y发生在那里,就像在C#中一样。这在Python 2.6中是否可行?目前看起来我能做的最好的(基于Glenn Maynard的答案)是:

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

这包括消息和跟踪信息,但不显示哪个异常在跟踪信息中的哪个位置发生。


3
已被接受的答案已经有些过时了,也许您应该考虑接受另一个答案。 - Russia Must Remove Putin
1
很遗憾,@AaronHall,自2015年以来我们没有看到过OP。 - Antti Haapala -- Слава Україні
如果我不想要外部跟踪信息怎么办?包装器只是为了添加更多信息,我不想在异常输出中混杂着(完美运行的)包装器代码的跟踪信息! - stam
9个回答

329

Python 3

在Python 3中,您可以执行以下操作:

try:
    raise MyExceptionToBeWrapped("I have twisted my ankle")

except MyExceptionToBeWrapped as e:

    raise MyWrapperException("I'm not in a good shape") from e
这将产生类似于这样的东西:
   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")

29
在Python 3中,“raise ... from ...”确实是正确的做法。这需要更多的赞同。 - Nakedible
“Nakedible” 我认为这是因为不幸的是大多数人仍然没有使用 Python 3。 - Tim Ludwinski
有没有一种替代语法可以在Python 3下实现相同的行为,但也可以包含在Python 2 + 3兼容的代码库中(在Python 2下不会导致“SyntaxError”)? - ogrisel
4
您可以使用future包来实现此操作:http://python-future.org/compatible_idioms.html#raising-exceptions。 例如:from future.utils import raise_raise_(ValueError, None, sys.exc_info()[2]) - jtpereyda
1
在使用“raise... from...”语法抛出异常后,我该如何在catch子句中获取这个“内部”异常? - ed22
显示剩余3条评论

143

Python 2

方法很简单:将 traceback 作为第三个参数传递给 raise。

import sys
class MyException(Exception): pass

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

当捕获一个异常并重新抛出另一个异常时,始终要这样做。


4
谢谢。这样可以保留回溯信息,但会丢失原始异常的错误消息。我怎样才能看到两个消息和两个回溯信息? - EMP
7
raise MyException(str(e)), ...等代码片段意为抛出一个包含错误信息的自定义异常。 - Glenn Maynard
25
Python 3 新增了 raise E() from tb.with_traceback(...) - Dima Tisnek
3
@GlennMaynard 这是一个相当古老的问题,但raise语句中的中间参数是要传递给异常的值(如果第一个参数是异常类而不是实例的情况下)。因此,如果您想交换异常,而不是使用raise MyException(str(e)), None, sys.exc_info()[2]'',最好使用这个:raise MyException,e.args,sys.exc_info()[2]``。 - bgusach
9
使用 future 包可以实现 Python2 和 Python3 兼容的方法:http://python-future.org/compatible_idioms.html#raising-exceptions,例如,`from future.utils import raise_raise_(ValueError, None, sys.exc_info()[2])`。 - jtpereyda
显示剩余5条评论

21

Python 3引入了raise ... from从句来链接异常。Glenn的回答对于Python 2.7非常好,但它只使用原始异常的回溯信息并丢弃错误消息和其他细节。以下是一些Python 2.7示例,它们将当前作用域中的上下文信息添加到原始异常的错误消息中,但保留其他细节。

已知异常类型

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()上的警告。

任何异常类型

这是另一个更通用的示例,如果您不知道代码可能需要捕获什么样的异常,则可以使用它。缺点是它会丢失异常类型并只引发一个RuntimeError。您必须导入traceback模块。

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

修改消息

如果异常类型允许您添加上下文信息,则可以选择另一种选项。您可以修改异常的消息,然后重新引发它。

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

这将生成以下堆栈跟踪:

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()的代码行,但异常信息现在包括命令行。

1
ex.strerror 是从哪里来的?我在 Python 文档中找不到任何相关信息。难道不应该是 str(ex) 吗? - Henrik Heimbuerger
1
IOError 是从 EnvironmentError 派生而来,提供了 errornostrerror 属性。 - Don Kirkby
我该如何通过捕获Exception将任意Error(例如ValueError)包装成RuntimeError?如果我按照您的答案进行操作,堆栈跟踪将丢失。 - Kalle Richter
我不确定你在问什么,@karl。你能否在一个新的问题中发布一个示例,然后从这里链接到它? - Don Kirkby
我编辑了我的问题副本,位于http://stackoverflow.com/questions/23157766/nested-causes-in-nested-exceptions-in-python/,并加入了一个澄清,直接考虑了你的答案。我们应该在那里讨论 :) - Kalle Richter
我明白,@karl,你想要像Java中的链接异常一样获取两个堆栈跟踪的详细信息。我不知道在Python 2中有什么方法可以做到这一点,但是阿列克谢的答案似乎涵盖了如何在Python 3中实现它的内容。 - Don Kirkby

15

Python 3.x 中:

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

或者只是简单的

except Exception:
    raise MyException()

如果未被处理,它将传播MyException但打印两个异常。

Python 2.x 中:

raise Exception, 'Failed to process file ' + filePath, e
你可以通过删除__context__属性来防止打印两个异常。在这里,我使用上述方法编写了一个上下文管理器,以便在运行时捕获并更改你的异常: (有关它们如何工作的解释,请参见http://docs.python.org/3.1/library/stdtypes.html)。
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

5
类型错误:抛出异常:第三个参数必须是traceback或者为None。 - Glenn Maynard
抱歉,我犯了一个错误,不知何故我以为它还可以自动接受异常并获取它们的回溯属性。根据http://docs.python.org/3.1/reference/simple_stmts.html#the-raise-statement,应该是e.__traceback__。 - ilya n.
1
@ilyan:Python 2没有e.__traceback__属性! - Jan Hudec

5
我认为在Python 2.x中你不能这样做,但类似于此功能的东西是Python 3的一部分。来自PEP 3134
在当前的Python实现中,异常由三部分组成:类型、值和回溯信息。'sys'模块将当前异常以三个并行变量ex_type、ex_value和ex_traceback公开,sys.exc_info()函数返回这三个部分的元组,并且'raise'语句具有接受这三个部分的三参数形式。经常需要同时传递这三个内容来操作异常,这可能很繁琐且容易出错。此外,'except'语句只能访问值,而无法访问回溯信息。向异常值添加'traceback'属性可以使所有异常信息从单个位置访问。
与C#的比较:
在C#中,异常包含一个只读的“InnerException”属性,该属性可能指向另一个异常。它的文档[10]表示:“当异常X作为先前异常Y的直接结果抛出时,X的InnerException属性应包含对Y的引用。”该属性不是由VM自动设置的;而是所有异常构造函数都需要一个可选的'innerException'参数来显式设置它。'cause'属性实现了与InnerException相同的目的,但是这个PEP提议一种新的'raise'形式,而不是扩展所有异常的构造函数。Java、Ruby和Perl 5也不支持这种类型的东西。再次引用:
关于其他编程语言,Java和Ruby在'catch'/'rescue'或'finally'/'ensure'从句中发生另一个异常时都会丢弃原始异常。Perl 5缺乏内置的结构化异常处理。对于Perl 6,RFC编号88 [9]提出了一种异常机制,它会隐式地将链接的异常保留在名为@@的数组中。

然而,在Perl5中,您当然可以只说“confess qq{OH NOES!$@}”而不会丢失其他异常的堆栈跟踪。或者您可以实现自己的类型以保留异常。 - jrockway
这个过时了吗?你可以使用 except Exception as err: 然后 raise WhateverError('failed while processing ' + x) from err,这样 "innerException" 就是 from 的东西,对吗? - doug65536
@doug65536 这是关于Python 2与Python 3的最新信息。 - Queeg

5
为了在Python 2和3之间实现最大的兼容性,您可以使用six库中的raise_from。这是您的示例(稍作修改以提高清晰度):https://six.readthedocs.io/#six.raise_from
import six

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

3

您可以使用我的CausedException类来链接Python 2.x中的异常(即使在Python 3中,如果您想将多个捕获的异常作为原因提供给新引发的异常,它也很有用)。也许它可以帮助您。


2
也许你可以获取相关信息并向上传递?我的想法是这样的:
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

2

假设:

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

您可以使用来自文档https://docs.python.org/3/tutorial/errors.html#raising-exceptions的简单解决方案:

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

输出结果:
An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
NameError: HiThere

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


这是兼容Python 2和3的解决方案!谢谢! - Andy Chase
我认为这个想法是引发不同类型的异常。 - Tim Ludwinski
2
这不是嵌套异常的链,只是重新引发一个异常。 - Kalle Richter
这是最好的Python 2解决方案,如果您只需要丰富异常消息并拥有完整的堆栈跟踪! - geekQ
使用 raise 和 raise from 有什么区别? - variable
输出结果会发送到标准输出(stdout)。如何使用logging记录这个追踪信息呢? - Prashanth Pradeep

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