python python-2.7 concurrency concurrent.futures

python - Obtención del número de línea original para excepción en concurrent.futures



python-2.7 concurrency (3)

Creo que el rastreo de excepción original se pierde en el código ThreadPoolExecutor. Almacena la excepción y luego la vuelve a elevar más tarde. Aquí hay una solución. Puede usar el módulo de rastreo para almacenar el mensaje de excepción original y el rastreo de su función f en una cadena. Luego genere una excepción con este mensaje de error, que ahora contiene el número de línea, etc. de f . El código que ejecuta f puede incluirse en un bloque try ... except , que detecta la excepción generada desde ThreadPoolExecutor e imprime el mensaje, que contiene el rastreo original.

El código de abajo funciona para mí. Creo que esta solución es un poco pirateada y preferiría poder recuperar el rastreo original, pero no estoy seguro de si eso es posible.

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"

Esto da el siguiente resultado en 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''

La razón por la que su código original proporciona la ubicación correcta cuando se ejecuta en Python 3 y no en 2.7 es que, en Python 3, las excepciones llevan el rastreo como un atributo, y cuando se vuelve a elevar una excepción, el rastreo se extiende en lugar de reemplazarse. El siguiente ejemplo ilustra esto:

def A(): raise BaseException("Fish") def B(): try: A() except BaseException as e: raise e B()

Corrí esto en Python 2.7 y Python 3.1 . En 2.7 la salida es la siguiente:

Traceback (most recent call last): File "exceptions.py", line 11, in <module> B() File "exceptions.py", line 9, in B raise e BaseException: Fish

es decir, el hecho de que la excepción fue originalmente lanzada desde A no se registra en la salida final. Cuando corro con Python 3.1 obtengo esto:

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

cual es mejor. Si sustituyo el raise e con solo el raise en el bloque excepto en B , entonces python2.7 proporciona el rastreo completo. Creo que al pasar por alto este módulo para python2.7 se pasaron por alto las diferencias en la propagación de excepciones.

Ejemplo de uso de concurrent.futures (backport para 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

Salida:

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''

La cadena ".../_base.py", line 356, in __get_result" no es el punto final que esperaba ver. ¿Es posible obtener una línea real donde se produjo la excepción? Algo como:

File "C:/test.py", line 3, in f return x * x # line 03

Python3 parece mostrar el número de línea correcto en este caso. ¿Por qué no puede python2.7? ¿Y hay alguna solución?


Estaba en su misma situación y realmente necesitaba tener el rastro de las excepciones planteadas. Pude desarrollar esta solución que consiste en usar la siguiente subclase de 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

Si usa esta subclase y ejecuta el siguiente fragmento de código:

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

La salida será algo como:

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

El problema está en el uso de sys.exc_info() por la biblioteca de futures . De la documentación:

Esta función devuelve una tupla de tres valores que proporcionan información sobre la excepción que se está manejando actualmente. [...] Si no se maneja ninguna excepción en ninguna parte de la pila, se devuelve una tupla que contiene tres valores Ninguno. De lo contrario, los valores devueltos son (tipo, valor, rastreo). Su significado es: tipo obtiene el tipo de excepción de la excepción que se maneja (un objeto de clase); el valor obtiene el parámetro de excepción (su valor asociado o el segundo argumento que se debe generar, que siempre es una instancia de clase si el tipo de excepción es un objeto de clase); traceback obtiene un objeto de rastreo que encapsula la pila de llamadas en el punto donde se produjo la excepción originalmente.

Ahora, si observa el código fuente de futures , podrá ver por sí mismo por qué se pierde el rastreo: cuando se genera una excepción y se establece en el objeto Future solo se sys.exc_info()[1] . Ver:

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)

Entonces, para evitar perder el rastreo, debes guardarlo en algún lugar. Mi solución es envolver la función para enviarla en un contenedor cuya única tarea es detectar todo tipo de excepción y generar una excepción del mismo tipo cuyo mensaje es el rastreo. Al hacer esto, cuando se sys.exc_info()[1] una excepción, el contenedor la captura y vuelve a subir, luego, cuando sys.exc_info()[1] se asigna a la excepción del objeto Future , el rastreo no se pierde.


Inspirándose en la primera respuesta, aquí está como decorador:

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

Solo aplica el decorador en la función ejecutada:

@reraise_with_stack def f(): pass