Подтвердить что ты не робот

Получение исходного номера строки для исключения в 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? И есть ли способ обхода?

4b9b3361

Ответ 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