如果不立即重新引发异常,则异常回溯信息将被隐藏。

77

我有一段类似于这样的代码:

import sys

def func1():
    func2()

def func2():
    raise Exception('test error')

def main():
    err = None

    try:
        func1()
    except:
        err = sys.exc_info()[1]
        pass

    # some extra processing, involving checking err details (if err is not None)

    # need to re-raise err so caller can do its own handling
    if err:
        raise err

if __name__ == '__main__':
    main()

func2 抛出异常时,我会收到以下回溯信息:

Traceback (most recent call last):
  File "err_test.py", line 25, in <module>
    main()
  File "err_test.py", line 22, in main
    raise err
Exception: test error

我从这里看不出异常来自哪里。原始的回溯信息已经丢失。

如何保留原始的回溯信息并重新引发它?我想看到类似于这样的东西:

Traceback (most recent call last):
  File "err_test.py", line 26, in <module>
    main()
  File "err_test.py", line 13, in main
    func1()
  File "err_test.py", line 4, in func1
    func2()
  File "err_test.py", line 7, in func2
    raise Exception('test error')
Exception: test error
7个回答

129

空的raise会引发最后一个异常。

# need to re-raise err so caller can do its own handling
if err:
    raise
如果你使用 raise something,Python 就无法知道 something 是前面刚刚捕获的异常还是一个新的带有新堆栈跟踪信息的异常。这就是为什么有一个空的 raise 语句可以保留堆栈跟踪信息的原因。
参考文献请见 这里

4
值得一提的是,这在 Python 3 中不起作用。 - yprez
22
yprez评论中的"This"指的是“离开except块后的空raise”。在Python 3中,仅在except块内部使用裸的“raise”才有效。 - jtniehof

72

可以通过修改并重新引发异常:

如果没有表达式存在,raise语句将重新引发在当前作用域中最后一次活跃的异常。如果当前作用域中不存在任何异常,则会抛出TypeError异常以指示这是一个错误(如果在IDLE下运行,则会引发Queue.Empty异常)。

否则,raise语句将评估表达式以获取三个对象,使用None作为省略表达式的值。前两个对象用于确定异常的类型和值。

如果存在第三个对象且不为None,则它必须是一个跟踪回溯对象(参见标准类型层次结构一节),并且它将被替换为异常发生地点的当前位置。如果第三个对象存在但不是跟踪回溯对象或None,则会引发TypeError异常。

raise语句的三个表达式形式有助于在except子句中透明地重新引发异常,但是如果要重新引发的异常是当前作用域中最近活动的异常,则应首选没有表达式的raise语句。

因此,如果要修改异常并重新引发它,则可以执行以下操作:

try:
    buggy_code_which_throws_exception()
except Exception as e:
    raise Exception, "The code is buggy: %s" % e, sys.exc_info()[2]

有趣。被接受的答案比这个更好地处理了OP的用例,但作为一个更一般的答案,这很有趣。不过我看不出它有多大用处,因为如果你捕获了一个ValueError并引发了一个RuntimeError,那么回溯信息就会变得非常误导人(你无法看到ValueError曾经涉及),而我个人遇到的唯一情况是我想保留回溯信息,但又想做一些比仅仅使用没有参数的raise更复杂的事情,这时我想引发一个不同类型的异常。 - Mark Amery
4
我用这个方法重新抛出同一个异常,但使用不同的消息来包含更多关于异常产生条件的细节信息。这些信息在外部作用域中是可用的,但在内部作用域中不可用。 - qris
1
有时使用这种三个表达式形式的raise是非常好的原因。你的答案帮助我编写了一个装饰器,它包装了集成测试,并在失败时拍摄屏幕截图。然后引发原始断言失败。问题在于,回溯被尝试/除外在截图代码中。所以谢谢! - aychedee
1
有没有一种在 Python 2 和 Python 3 中都兼容的方法来做这件事?我在 Python 3 中遇到了 SyntaxError。 - Elias Dorneles
6
@six.reraise(exc_type, exc_value, exc_traceback=None)是一个函数,用于重新抛出异常。它将异常的类型,值和可选的回溯信息作为参数,并将该异常重新引发。 - qris

8
你可以通过sys.exc_info()traceback模块获取有关异常的大量信息。尝试将以下扩展添加到您的代码中。
import sys
import traceback

def func1():
    func2()

def func2():
    raise Exception('test error')

def main():

    try:
        func1()
    except:
        exc_type, exc_value, exc_traceback = sys.exc_info()
        # Do your verification using exc_value and exc_traceback

        print "*** print_exception:"
        traceback.print_exception(exc_type, exc_value, exc_traceback,
                                  limit=3, file=sys.stdout)

if __name__ == '__main__':
    main()

这将会打印出与你想要的类似的内容。
*** print_exception:
Traceback (most recent call last):
  File "err_test.py", line 14, in main
    func1()
  File "err_test.py", line 5, in func1
    func2()
  File "err_test.py", line 8, in func2
    raise Exception('test error')
Exception: test error

2
不,我不想在 main() 中打印它。我想用原始的回溯重新引发它,并让 main() 的调用者处理它(例如忽略、打印到控制台、保存到数据库等)。Jochen 的解决方案有效。 - parxier
2
如果你将 print 更改为类似于 raise exc_type.with_traceback(exc_value, exc_traceback) 的内容,那么这将是 Python3 的最佳答案。 - Davos

5
虽然 @Jochen 的回答在简单情况下效果很好,但它无法处理更复杂的情况,例如当你不直接捕获和重新抛出异常,而是以某种原因将异常作为一个对象并希望在全新的上下文中重新抛出时(也就是说,如果你需要在不同的进程中处理它)。
在这种情况下,我建议如下操作:
  1. 获取原始 exc_info
  2. 格式化原始错误消息及其堆栈跟踪
  3. 使用完整的错误消息(包含堆栈跟踪)抛出新的异常
在执行此操作之前,请定义一个新的异常类型以便稍后重新抛出...
class ChildTaskException(Exception):
    pass

在有问题的代码中...
import sys
import traceback

try:
    # do something dangerous
except:
    error_type, error, tb = sys.exc_info()
    error_lines = traceback.format_exception(error_type, error, tb)
    error_msg = ''.join(error_lines)
    # for example, if you are doing multiprocessing, you might want to send this to another process via a pipe
    connection.send(error_msg)

重新抛出异常...

# again, a multiprocessing example of receiving that message through a pipe
error_msg = pcon.recv()
raise ChildTaskException(error_msg)

2

你的主函数应该是这样的:

def main():
    try:
        func1()
    except Exception, err:
        # error processing
        raise

这是处理(和重新抛出)错误的标准方式。 这里有一个CodePad演示。


我有一种感觉,使用旧式的“raise”方法抛出异常:“raise“bad exception””可能会绕过“except Exception, err:”语句块。 - parxier
@parxier 然后使用 except object, err - Gabi Purcaru
这与 err = sys.exc_info()[1] 没有任何区别。无论如何,主要问题是在不丢失原始回溯信息的情况下,在 except 块之外重新引发 err。Jochen 的解决方案可行。 - parxier

2

在Python 3中:

import sys

class CustomError(Exception):
    pass

try:
    code_throwing_an_exception()
except Exception as e:
    _, value, traceback = sys.exc_info()
    raise CustomError("A new Exception was raised: %s" % value).with_traceback(traceback)

0
正如Jochen所提到的,Python无法知道err是一个新的异常还是被重新引发的已捕获异常。在这里,from子句可以帮助解决这个问题。 我有一个名为test.py的文件,其中包含以下代码:
try:
    raise RuntimeError("Original error")
except RuntimeError as e:
    err = e

raise RuntimeError("Previous operation failed") from err

结果是合理的。
$ python test.py
Traceback (most recent call last):
  File "/home/valajeyhani/development/repositories/geotab/image_builder/test.py", line 2, in <module>
    raise RuntimeError("Original error")
RuntimeError: Original error

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

Traceback (most recent call last):
  File "/home/valajeyhani/development/repositories/geotab/image_builder/test.py", line 6, in <module>
    raise RuntimeError("Previous operation failed") from err
RuntimeError: Previous operation failed

查看this以获取Python2中from的支持。
如果在您的特定应用程序中,异常的链式表示不理想,我可以想到两种替代解决方案。
try:
    raise RuntimeError("Original error")
except RuntimeError as e:
    err = e

raise err from None

结果:

$ python test.py
Traceback (most recent call last):
  File "/home/valajeyhani/development/repositories/geotab/image_builder/test.py", line 6, in <module>
    raise err from None
  File "/home/valajeyhani/development/repositories/geotab/image_builder/test.py", line 2, in <module>
    raise RuntimeError("Original error")
RuntimeError: Original error

这不是原始的回溯,但至少显示了“原始”异常的正确来源,即第2行。
最后,第二种方法不会链接异常。
try:
    raise RuntimeError("Original error")
except RuntimeError as e:
    err = e

raise err.with_traceback(err.__traceback__)

输出如下:
$ python test.py
Traceback (most recent call last):
  File "/home/valajeyhani/development/repositories/geotab/image_builder/test.py", line 6, in <module>
    raise err.with_traceback(err.__traceback__)
  File "/home/valajeyhani/development/repositories/geotab/image_builder/test.py", line 2, in <module>
    raise RuntimeError("Original error")
RuntimeError: Original error

结果与之前的方法非常相似。

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