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

Posted

技术标签:

【中文标题】Python中的“内部异常”(带有回溯)?【英文标题】:"Inner exception" (with traceback) in Python? 【发布时间】:2010-11-23 22:53:50 【问题描述】:

我的背景是 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]

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

【问题讨论】:

接受的答案已经过时了,也许你应该考虑接受另一个。 @AaronHall 不幸的是 OP 自 2015 年以来就没有出现过。 【参考方案1】:

我认为您不能在 Python 2.x 中执行此操作,但类似于此功能的功能是 Python 3 的一部分。来自PEP 3134:

在今天的 Python 实现中,异常由三部分组成 部分:类型、值和回溯。 “系统”模块, 在三个并行变量 exc_type 中公开当前异常, exc_value 和 exc_traceback,sys.exc_info() 函数返回一个 这三个部分的元组,“raise”语句有一个 接受这三个部分的三参数形式。操纵 异常通常需要并行传递这三件事, 这可能很乏味且容易出错。此外,“除外” 语句只能提供对值的访问,而不是回溯。 将 'traceback' 属性添加到异常值会使所有 可以从一个地方访问的异常信息。

与 C# 的比较:

C# 中的异常包含一个只读的“InnerException”属性,该属性 可能指向另一个异常。它的文档 [10] 说 “当异常 X 作为先前的直接结果而引发时 异常 Y,X 的 InnerException 属性应包含 引用 Y。” 该属性不是由 VM 自动设置的; 相反,所有异常构造函数都采用可选的“innerException” 参数来明确设置它。 'cause' 属性满足 目的与 InnerException 相同,但此 PEP 提出了一种新形式 'raise' 而不是扩展所有异常的构造函数。 C# 还提供了一个 GetBaseException 方法,可以直接跳转到 InnerException 链的末端;这个 PEP 没有提出类似的建议。

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

至于其他语言,Java 和 Ruby 都抛弃了原有的 'catch'/'rescue' 中发生另一个异常时的异常或 'finally'/'ensure' 子句。 Perl 5 缺乏内置的结构化 异常处理。对于 Perl 6,RFC 编号 88 [9] 提出了一个例外 在数组中隐式保留链式异常的机制 命名为@@。

【讨论】:

但是,当然,在 Perl5 中,您可以只说“confess qqOH NOES! $@”而不会丢失其他异常的堆栈跟踪。或者您可以实现自己的保留异常的类型。 这是过时的还是什么?你可以except Exception as err: 然后raise WhateverError('failed while processing ' + x) from err 而“innerException”就是from 的东西,对吧?【参考方案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

【讨论】:

【参考方案3】:

Python 3.x 中:

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

只是

except Exception:
    raise MyException()

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

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

【讨论】:

TypeError: raise: arg 3 must be a traceback or None 对不起,我犯了一个错误,不知何故我以为它也接受异常并自动获取它们的回溯属性。根据docs.python.org/3.1/reference/…,这应该是 e.__traceback__ @ilyan.: Python 2 没有e.__traceback__ 属性!【参考方案4】:

Python 2

这很简单;将回溯作为第三个参数传递给 raise。

import sys
class MyException(Exception): pass

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

在捕获一个异常并重新引发另一个异常时总是这样做。

【讨论】:

谢谢。这会保留回溯,但会丢失原始异常的错误消息。如何同时查看消息和回溯? raise MyException(str(e)), ... Python 3 添加了raise E() from tb.with_traceback(...) @GlennMaynard 这是一个很老的问题,但是raise 的中间参数是传递给异常的值(如果第一个参数是异常类而不是实例)。所以如果你想交换异常,而不是做raise MyException(str(e)), None, sys.exc_info()[2],最好使用这个:raise MyException, e.args, sys.exc_info()[2] 使用 future 包可以实现 Python2 和 3 兼容的方式:python-future.org/compatible_idioms.html#raising-exceptions E.g. from future.utils import raise_raise_(ValueError, None, sys.exc_info()[2]).【参考方案5】:

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")

【讨论】:

raise ... from ... 确实是在 Python 3 中执行此操作的正确方法。这需要更多的支持。 Nakedible 我认为这是因为不幸的是大多数人仍然没有使用 Python 3。 即使在 python 3 中使用 'from' 似乎也会发生这种情况 可以向后移植到 Python 2。希望有朝一日。 @ogrisel 您可以使用future 包来实现此目的:python-future.org/compatible_idioms.html#raising-exceptions E.g. from future.utils import raise_raise_(ValueError, None, sys.exc_info()[2]).【参考方案6】:

假设:

您需要一个适用于 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 兼容的解决方案!谢谢! 我认为这个想法是引发不同类型的异常。 这不是一连串的嵌套异常,只是重新引发一个异常 这是最好的 python 2 解决方案,如果您只需要丰富异常消息并拥有完整的堆栈跟踪! 使用 raise 和 raise from 有什么区别【参考方案7】:

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

【讨论】:

【参考方案8】:

Python 3 有 raise ... from clause 来链接异常。 Glenn's answer 非常适合 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 statement 的那种风格将异常类型作为第一个表达式,将元组中的异常类构造函数参数作为第二个表达式,将回溯作为第三个表达式。如果您运行的版本早于 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() 的行,但异常消息现在包含命令行。

【讨论】:

ex.strerror 来自哪里?我在 Python 文档中找不到任何相关的命中。不应该是str(ex)吗? IOError 派生自EnvironmentError、@hheimbuerger,后者提供errornostrerror 属性。 我将如何包装任意Error,例如ValueError,通过捕获Exception 进入RuntimeError?如果我重现您对这种情况的回答,堆栈跟踪就会丢失。 我不确定你在问什么,@karl。你能在一个新问题中发布一个示例,然后从这里链接到它吗? 我在***.com/questions/23157766/… 编辑了我对OP 问题的副本,并直接考虑了您的回答。我们应该在那里讨论:)【参考方案9】:

为了最大限度地实现 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)

【讨论】:

以上是关于Python中的“内部异常”(带有回溯)?的主要内容,如果未能解决你的问题,请参考以下文章

Python中的回溯[2]

“自我”如何正确更新原始变量? N皇后问题中的递归/回溯(Python)

如何使用 Python 修复 Pyx 模块中的回溯错误?

查找带有回溯的数字组合

如何使用 python 日志框架在带有回溯的警告或信息级别记录异常?

牛顿法梯度下降算法中的 TypeError 和 ValueError 回溯