在 concurrent.futures 中获取异常的原始行号
Posted
技术标签:
【中文标题】在 concurrent.futures 中获取异常的原始行号【英文标题】:Getting original line number for exception in concurrent.futures 【发布时间】:2013-10-19 00:35:29 【问题描述】:使用 concurrent.futures 的示例(2.7 的反向移植):
import concurrent.futures # line 01
def f(x): # line 02
return x * x # line 03
data = [1, 2, 3, None, 5] # line 04
with concurrent.futures.ThreadPoolExecutor(len(data)) as executor: # line 05
futures = [executor.submit(f, n) for n in data] # line 06
for future in futures: # line 07
print(future.result()) # line 08
输出:
1
4
9
Traceback (most recent call last):
File "C:\test.py", line 8, in <module>
print future.result() # line 08
File "C:\dev\Python27\lib\site-packages\futures-2.1.4-py2.7.egg\concurrent\futures\_base.py", line 397, in result
return self.__get_result()
File "C:\dev\Python27\lib\site-packages\futures-2.1.4-py2.7.egg\concurrent\futures\_base.py", line 356, in __get_result
raise self._exception
TypeError: unsupported operand type(s) for *: 'NoneType' and 'NoneType'
字符串"...\_base.py", line 356, in __get_result"
不是我希望看到的端点。是否可以获得引发异常的真实线路?比如:
File "C:\test.py", line 3, in f
return x * x # line 03
在这种情况下,Python3 似乎显示了正确的行号。为什么python2.7不能?有什么解决方法吗?
【问题讨论】:
我也在寻找这个问题的答案。谢谢! 【参考方案1】:从第一个答案中获得灵感,这里作为装饰器:
import functools
import traceback
def reraise_with_stack(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
traceback_str = traceback.format_exc(e)
raise StandardError("Error occurred. Original traceback "
"is\n%s\n" % traceback_str)
return wrapped
只需在执行的函数上应用装饰器:
@reraise_with_stack
def f():
pass
【讨论】:
StandardError
has been removed with python3.【参考方案2】:
我认为原始异常回溯在 ThreadPoolExecutor 代码中丢失了。它存储异常,然后稍后重新引发它。这是一种解决方案。您可以使用 traceback 模块将原始异常消息和来自函数 f 的回溯存储到字符串中。然后使用此错误消息引发异常,该错误消息现在包含 f 的行号等。运行 f 的代码可以包装在 try...except 块中,该块捕获从 ThreadPoolExecutor 引发的异常,并打印消息,其中包含原始回溯。
下面的代码对我有用。我认为这个解决方案有点hacky,并且希望能够恢复原始回溯,但我不确定这是否可能。
import concurrent.futures
import sys,traceback
def f(x):
try:
return x * x
except Exception, e:
tracebackString = traceback.format_exc(e)
raise StandardError, "\n\nError occurred. Original traceback is\n%s\n" %(tracebackString)
data = [1, 2, 3, None, 5] # line 10
with concurrent.futures.ThreadPoolExecutor(len(data)) as executor: # line 12
try:
futures = [executor.submit(f, n) for n in data] # line 13
for future in futures: # line 14
print(future.result()) # line 15
except StandardError, e:
print "\n"
print e.message
print "\n"
这在 python2.7 中给出了以下输出:
1
4
9
Error occurred. Original traceback is
Traceback (most recent call last):
File "thread.py", line 8, in f
return x * x
TypeError: unsupported operand type(s) for *: 'NoneType' and 'NoneType'
您的原始代码在 Python 3 而不是 2.7 中运行时给出正确位置的原因是,在 Python 3 中,异常将回溯作为属性,并且在重新引发异常时,回溯被扩展而不是被替换。下面的例子说明了这一点:
def A():
raise BaseException("Fish")
def B():
try:
A()
except BaseException as e:
raise e
B()
我在 python 2.7 和 python 3.1 中运行了这个。 2.7中的输出如下:
Traceback (most recent call last):
File "exceptions.py", line 11, in <module>
B()
File "exceptions.py", line 9, in B
raise e
BaseException: Fish
即异常最初是从 A 引发的这一事实并未记录在最终输出中。当我使用 python 3.1 运行时,我得到了这个:
Traceback (most recent call last):
File "exceptions.py", line 11, in <module>
B()
File "exceptions.py", line 9, in B
raise e
File "exceptions.py", line 7, in B
A()
File "exceptions.py", line 3, in A
raise BaseException("Fish")
BaseException: Fish
哪个更好。如果我在 B 的 except 块中将 raise e
替换为 raise
,那么 python2.7 会给出完整的回溯。我的猜测是,当将此模块反向移植到 python2.7 时,异常传播的差异被忽略了。
【讨论】:
感谢您的详细解释,抱歉迟到了。【参考方案3】:我遇到了同样的情况,我真的需要追溯引发的异常。
我能够开发出这种解决方法,其中包括使用以下子类
ThreadPoolExecutor
.
import sys
import traceback
from concurrent.futures import ThreadPoolExecutor
class ThreadPoolExecutorStackTraced(ThreadPoolExecutor):
def submit(self, fn, *args, **kwargs):
"""Submits the wrapped function instead of `fn`"""
return super(ThreadPoolExecutorStackTraced, self).submit(
self._function_wrapper, fn, *args, **kwargs)
def _function_wrapper(self, fn, *args, **kwargs):
"""Wraps `fn` in order to preserve the traceback of any kind of
raised exception
"""
try:
return fn(*args, **kwargs)
except Exception:
raise sys.exc_info()[0](traceback.format_exc()) # Creates an
# exception of the
# same type with the
# traceback as
# message
如果你使用这个子类并运行以下sn-p:
def f(x):
return x * x
data = [1, 2, 3, None, 5]
with ThreadPoolExecutorStackTraced(max_workers=len(data)) as executor:
futures = [executor.submit(f, n) for n in data]
for future in futures:
try:
print future.result()
except TypeError as e:
print e
输出将类似于:
1
4
9
Traceback (most recent call last):
File "future_traceback.py", line 17, in _function_wrapper
return fn(*args, **kwargs)
File "future_traceback.py", line 24, in f
return x * x
TypeError: unsupported operand type(s) for *: 'NoneType' and 'NoneType'
25
问题在于futures
库对sys.exc_info()
的使用。来自
文档:
此函数返回一个包含三个值的元组,这些值提供有关异常的信息 目前正在处理中。 [...] 如果堆栈上的任何地方都没有处理异常,则包含三个 None 值的元组是 回来。否则,返回的值为(类型、值、回溯)。他们的意思是:类型获取 正在处理的异常的异常类型(类对象);值获取异常 参数(它的关联值或 raise 的第二个参数,它始终是一个类实例 如果异常类型是类对象); traceback 获取一个封装了 在最初发生异常的位置调用堆栈。
现在,如果您查看futures
的源代码,您可以自己了解为什么要进行回溯
丢失:当引发异常并且仅将其设置为 Future
对象时
sys.exc_info()[1]
已通过。见:
https://code.google.com/p/pythonfutures/source/browse/concurrent/futures/thread.py (L:63) https://code.google.com/p/pythonfutures/source/browse/concurrent/futures/_base.py (L:356)
因此,为避免丢失回溯,您必须将其保存在某个地方。我的解决方法是包装
提交到包装器的函数,其唯一任务是捕获各种异常和
引发相同类型的异常,其消息是回溯。通过这样做,当一个
引发异常,它被包装器捕获并重新引发,然后当sys.exc_info()[1]
分配给Future
对象的异常,回溯不丢失。
【讨论】:
我看了很多关于 SO 的答案,这个对我有用 - 谢谢。我无法使用其他方法(甚至是最近的方法)获取带有行号的堆栈跟踪。奇怪的是,这仍然是并发期货的问题。 优雅的 OOP 解决方案,正确使用覆盖。谢谢! 四年后图书馆里还有更多的东西吗?我认为使用concurrent.futures.ProcessPoolExecutor
可能需要更多技巧,因为如果不进行更改,submit
方法将在“无法腌制'weakref'对象”上失败
好主意。我想你可能想诱捕BaseException
,而不仅仅是Exception
。以上是关于在 concurrent.futures 中获取异常的原始行号的主要内容,如果未能解决你的问题,请参考以下文章
在 python 的 concurrent.futures 中查找 BrokenProcessPool 的原因
从 concurrent.futures 到 asyncio
为啥我不能在类方法中使用 python 模块 concurrent.futures?
使用 concurrent.futures.ThreadPoolExecutor() 时的 PyQt5 小部件 Qthread 问题