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