Получение исходного номера строки для исключения в concurrent.futures
Пример использования concurrent.futures(backport for 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'
String "...\_base.py", line 356, in __get_result"
- это не конечная точка, которую я ожидал увидеть. Можно ли получить реальную строку, где было выбрано исключение? Что-то вроде:
File "C:\test.py", line 3, in f
return x * x # line 03
В этом случае Python3 показывает правильный номер строки. Почему не может python2.7? И есть ли способ обхода?
Ответы
Ответ 1
Я думаю, что исходный трассировка исключения теряется в коде ThreadPoolExecutor. Он хранит исключение, а затем повторно создает его позже. Вот одно решение. Вы можете использовать модуль traceback, чтобы сохранить исходное сообщение об исключении и трассировку из вашей функции f в строку. Затем создайте исключение с этим сообщением об ошибке, которое теперь содержит номер строки и т.д. F. Код, который запускает f, может быть завернут в блок try... except, который улавливает исключение, созданное из ThreadPoolExecutor, и печатает сообщение, содержащее исходную трассировку.
Код ниже работает для меня. Я думаю, что это решение немного взломано и предпочитает восстанавливать исходную трассу, но я не уверен, что это возможно.
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
что лучше. Если вместо raise
заменить raise e
на блок исключений в B, то python2.7 дает полную трассировку. Я предполагаю, что при обратном переносе этого модуля для python2.7 были исключены различия в распространении исключений.
Ответ 2
Я был в вашей ситуации, и мне действительно нужно было отследить возвышенные исключения.
Я смог разработать это обходное решение, которое заключается в использовании следующего подкласса
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
Если вы используете этот подкласс и запустите следующий фрагмент:
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
Проблема заключается в использовании sys.exc_info()
библиотекой futures
. Из
документация:
Эта функция возвращает кортеж из трех значений, которые предоставляют информацию об исключении, которое в настоящее время обрабатывается. [...] Если исключение не обрабатывается в любом месте стека, кортеж, содержащий три значения None, вернулся. В противном случае возвращаемые значения (тип, значение, трассировка). Их значение: тип получает тип исключения обрабатываемого исключения (объект класса); значение получает исключение параметр (его связанное значение или второй аргумент для повышения, который всегда является экземпляром класса если тип исключения - объект класса); traceback получает объект трассировки, который инкапсулирует вызывать стек в точке, где первоначально происходило исключение.
Теперь, если вы посмотрите на исходный код futures
, вы можете сами увидеть, почему трассировка
lost: когда возникает исключение, и оно должно быть установлено только для объекта 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
, трассировка не теряется.
Ответ 3
Вдохновляясь первым ответом, здесь он как декоратор:
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