重新引发Python异常并保留堆栈跟踪

57

我正在尝试在线程中捕获异常并在主线程中重新引发它:

import threading
import sys

class FailingThread(threading.Thread):
    def run(self):
        try:
            raise ValueError('x')
        except ValueError:
            self.exc_info = sys.exc_info()

failingThread = FailingThread()
failingThread.start()
failingThread.join()

print failingThread.exc_info
raise failingThread.exc_info[1]

这基本上可以正常工作,并产生以下输出:

(<type 'exceptions.ValueError'>, ValueError('x',), <traceback object at 0x1004cc320>)
Traceback (most recent call last):
  File "test.py", line 16, in <module>
    raise failingThread.exc_info[1]

然而,异常的源头指向了第16行,这是重新引发异常的地方。原始异常来自于第7行。我需要如何修改主线程,以便输出结果为:

Traceback (most recent call last):
  File "test.py", line 7, in <module>

太棒了,我也曾经从其他线程重新引发异常,但从来没有像你想的那样深入。 - Dima Tisnek
可能是["Inner exception" (with traceback) in Python?]的重复问题。(https://dev59.com/6HM_5IYBdhLWcg3wgjW2) - Mr_and_Mrs_D
2个回答

54
在Python 2中,你需要使用所有三个参数来触发异常:
raise failingThread.exc_info[0], failingThread.exc_info[1], failingThread.exc_info[2]

将回溯对象作为第三个参数传递可以保留堆栈。

来自help('raise'):

如果存在第三个对象并且不为None,它必须是一个回溯对象(参见标准类型层次结构),并且它被替换为发生异常的位置而非当前位置。如果第三个对象存在且不是回溯对象或None,则会引发TypeError异常。raise的三表达式形式在except子句中透明地重新引发异常很有用,但是如果要重新引发的异常是当前范围内最近活动的异常,则应优先使用没有表达式的raise

在这种特定情况下,您不能使用无表达式版本。

对于Python 3(根据评论):

raise failingThread.exc_info[1].with_traceback(failingThread.exc_info[2])

或者你可以简单地使用 raise ... from ... 链接异常,但这会在 cause 属性中附加原始上下文并引发一个链式异常,可能或可能不是你想要的。


6
@AndyHayden 在Python 3中,raise语句的调用方式需要像raise failingThread.exc_info[0](failingThread.exc_info[1]).with_traceback(failingThread.exc_info[2])那样。尽管在Python 3中,raise AnotherError from stored_exception可能会提供更好的输出。 - Tobias Kienzler
1
更正一下,根据这个评论,第一个应该是raise failingThread.exc_info[1].with_traceback(failingThread.exc_info[2]) - Tobias Kienzler
@TobiasKienzler,您介意将您高赞的评论扩展为一个新答案将其编辑到当前答案中吗?在Python 2时代之后,仅支持Python 2的解决方案已经过时了。 - Cecil Curry
2
@CecilCurry 好的建议,我已经更新了我的答案。不过你肯定有足够的权限可以直接编辑它吧? - Duncan

3
这段代码片段适用于Python 2和3两个版本:
      1 try:
----> 2     raise KeyError('Default key error message')
      3 except KeyError as e:
      4     e.args = ('Custom message when get re-raised',) #The comma is not a typo, it's there to indicate that we're replacing the tuple that e.args pointing to with another tuple that contain the custom message.
      5     raise

2
我可以猜到为什么会被踩。如果没有任何异常处理,这个解决方案是有效的。想象一下,当捕获到异常时,您尝试更新数据库中的状态,但那也失败了。在这种情况下,将引发最后一个异常(数据库事务失败),而不是最初捕获的异常。 - odedfos

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