Python 中的“内部异常”(带有回溯)?
- 2025-04-16 08:57:00
- admin 原创
- 14
问题描述:
我之前是 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]
这包括消息和回溯,但没有显示回溯中发生了哪个异常。
解决方案 1:
Python 3
在python3中你可以执行以下操作:
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")
解决方案 2:
Python 2
这很简单;将回溯作为第三个参数传递给 raise。
import sys
class MyException(Exception): pass
try:
raise TypeError("test")
except TypeError, e:
raise MyException(), None, sys.exc_info()[2]
当捕获一个异常并重新引发另一个异常时,总是这样做。
解决方案 3:
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()
,但是异常消息现在包含命令行。
解决方案 4:
在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:
我认为你不能在 Python 2.x 中做到这一点,但与此功能类似的功能是 Python 3 的一部分。来自PEP 3134:
在当今的 Python 实现中,异常由三部分组成:类型、值和回溯。“sys”模块将当前异常暴露在三个并行变量中:exc_type、exc_value 和 exc_traceback;sys.exc_info() 函数返回由这三部分组成的元组;“raise”语句则以三参数形式接受这三部分。处理异常通常需要并行传递这三部分,这既繁琐又容易出错。此外,“except”语句只能访问值,而不能访问回溯。为异常值添加“ traceback ”属性,即可从一处访问所有异常信息。
与 C# 的比较:
C# 中的异常包含一个只读的“InnerException”属性,该属性可能指向另一个异常。其文档 [10] 指出:“当异常 X 作为前一个异常 Y 的直接结果而引发时,X 的 InnerException 属性应包含对 Y 的引用。” 此属性并非由虚拟机自动设置;相反,所有异常构造函数都接受一个可选的“innerException”参数来显式设置它。“ cause ”属性实现与 InnerException 相同的目的,但本 PEP 提出了一种新的“raise”形式,而不是扩展所有异常的构造函数。C# 还提供了一个 GetBaseException 方法,可以直接跳转到 InnerException 链的末尾;本 PEP 未提出任何类似方法。
还要注意的是,Java、Ruby 和 Perl 5 也不支持这种类型。再次引用:
与其他语言一样,当“catch”/“rescue”或“finally”/“ensure”子句中出现另一个异常时,Java 和 Ruby 都会丢弃原始异常。Perl 5 缺少内置的结构化异常处理。对于 Perl 6,RFC 编号 88 [9] 提出了一种异常机制,该机制将链式异常隐式地保存在名为 @@ 的数组中。
解决方案 6:
为了最大程度地兼容 Python 2 和 3,您可以使用库raise_from
中的six
代码。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)
解决方案 7:
也许你可以把相关信息分享一下?我的想法是这样的:
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
解决方案 8:
您可以使用我的CausedException 类在 Python 2.x 中链接异常(即使在 Python 3 中,如果您想将多个捕获的异常作为新引发的异常的原因,它也很有用)。也许它能帮到您。
解决方案 9:
假设:
你需要一个适用于 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 块中重新引发异常。
扫码咨询,免费领取项目管理大礼包!