“Inner exception” (with traceback) in Python?
我的背景是C,我最近刚开始用Python编程。当抛出一个异常时,我通常希望将它包装在另一个异常中,这样可以添加更多信息,同时仍然显示完整的堆栈跟踪。在C中这很容易,但是在Python中我该怎么做呢?
在C中,我会这样做:
1 2 3 4 5 6 7 8 | try { ProcessFile(filePath); } catch (Exception ex) { throw new ApplicationException("Failed to process file" + filePath, ex); } |
在python中,我可以做类似的事情:
1 2 3 4 | try: ProcessFile(filePath) except Exception as e: raise Exception('Failed to process file ' + filePath, e) |
…但这会丢失内部异常的回溯!
编辑:我想看到异常消息和两个堆栈跟踪,并将它们关联起来。也就是说,我想在输出中看到这里发生了异常X,然后在那里发生了异常Y——和在C中一样。这在python 2.6中是可能的吗?目前为止我能做的最好的(基于格伦·梅纳德的回答)是:
1 2 3 4 | try: ProcessFile(filePath) except Exception as e: raise Exception('Failed to process file' + filePath, e), None, sys.exc_info()[2] |
这包括消息和两个回溯,但它不显示在回溯中发生的异常。
Python 3
在Python3中,可以执行以下操作:
1 2 3 4 5 6 | try: raise MyExceptionToBeWrapped("I have twisted my ankle") except MyExceptionToBeWrapped as e: raise MyWrapperException("I'm not in a good shape") from e |
这将产生如下结果:
1 2 3 4 5 6 7 8 9 | 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") |
Python 2
这很简单;把回溯作为第三个要提出的参数传递。
1 2 3 4 5 6 7 | import sys class MyException(Exception): pass try: raise TypeError("test") except TypeError, e: raise MyException(), None, sys.exc_info()[2] |
当捕获一个异常并重新引发另一个异常时,请始终执行此操作。
python 3有
1 2 3 4 5 6 7 8 | 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 |
这种风格的
这是另一个更通用的例子,如果您不知道代码可能必须捕获哪种异常的话。缺点是它会丢失异常类型并只引发一个运行时错误。您必须导入
1 2 3 4 5 | 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 |
修改消息
这是另一个选项,如果异常类型允许您向它添加上下文。您可以修改异常的消息,然后重新发出它。
1 2 3 4 5 6 7 8 | 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 |
生成以下堆栈跟踪:
1 2 3 4 5 6 7 8 9 10 | 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'] |
您可以看到它显示了调用
在Python 3。
1 | raise Exception('Failed to process file ' + filePath).with_traceback(e.__traceback__) |
或者简单地
1 2 | except Exception: raise MyException() |
它将传播
在Python 2。
1 | raise Exception, 'Failed to process file ' + filePath, e |
通过取消
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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 |
我认为在python2.x中不能这样做,但是类似于此功能的东西是python3的一部分。来自PEP 3134:
In today's Python implementation, exceptions are composed of three
parts: the type, the value, and the traceback. The 'sys' module,
exposes the current exception in three parallel variables, exc_type,
exc_value, and exc_traceback, the sys.exc_info() function returns a
tuple of these three parts, and the 'raise' statement has a
three-argument form accepting these three parts. Manipulating
exceptions often requires passing these three things in parallel,
which can be tedious and error-prone. Additionally, the 'except'
statement can only provide access to the value, not the traceback.
Adding the 'traceback' attribute to exception values makes all
the exception information accessible from a single place.
与C比较:
Exceptions in C# contain a read-only 'InnerException' property that
may point to another exception. Its documentation [10] says that
"When an exception X is thrown as a direct result of a previous
exception Y, the InnerException property of X should contain a
reference to Y." This property is not set by the VM automatically;
rather, all exception constructors take an optional 'innerException'
argument to set it explicitly. The 'cause' attribute fulfills
the same purpose as InnerException, but this PEP proposes a new form
of 'raise' rather than extending the constructors of all exceptions.
C# also provides a GetBaseException method that jumps directly to
the end of the InnerException chain; this PEP proposes no analog.
还要注意,Java、Ruby和Perl 5也不支持这种类型的东西。再次引用:
As for other languages, Java and Ruby both discard the original
exception when another exception occurs in a 'catch'/'rescue' or
'finally'/'ensure' clause. Perl 5 lacks built-in structured
exception handling. For Perl 6, RFC number 88 [9] proposes an exception
mechanism that implicitly retains chained exceptions in an array
named @@.
您可以使用我的causedException类在python2.x中链接异常(即使在python3中,如果您想将多个捕获的异常作为引发新异常的原因,它也很有用)。也许它能帮助你。
假设:
- 您需要一个适用于python2的解决方案(对于纯python3,请参见
raise ... from 解决方案) - 只想丰富错误消息,例如提供一些附加上下文
- 需要完整的堆栈跟踪
您可以使用文档https://docs.python.org/3/tutorial/errors.html中的简单解决方案引发异常:
1 2 3 4 5 | 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 |
输出:
1 2 3 4 | An exception flew by! Traceback (most recent call last): File"<stdin>", line 2, in ? NameError: HiThere |
看起来关键部分是独立的简化的"raise"关键字。这将在except块中重新引发异常。
也许你可以抓到相关的信息然后把它传出去?我在想:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 |
为了在python 2和3之间实现最大的兼容性,可以在
1 2 3 4 5 6 | import six try: ProcessFile(filePath) except Exception as e: six.raise_from(IOError('Failed to process file ' + repr(filePath)), e) |