¿"Excepción interna" (con rastreo) en Python?

146

Mi experiencia es en C # y recientemente comencé a programar en Python. Cuando se produce una excepción, normalmente quiero incluirla en otra excepción que agregue más información y, al mismo tiempo, muestre el seguimiento completo de la pila. Es bastante fácil en C #, pero ¿cómo lo hago en Python?

P.ej. en C # haría algo como esto:

try
{
  ProcessFile(filePath);
}
catch (Exception ex)
{
  throw new ApplicationException("Failed to process file " + filePath, ex);
}

En Python puedo hacer algo similar:

try:
  ProcessFile(filePath)
except Exception as e:
  raise Exception('Failed to process file ' + filePath, e)

... pero esto pierde el rastro de la excepción interna!

Editar: me gustaría ver tanto los mensajes de excepción como los seguimientos de pila y correlacionar los dos. Es decir, quiero ver en la salida que la excepción X ocurrió aquí y luego la excepción Y allí, igual que lo haría en C #. ¿Es esto posible en Python 2.6? Parece que lo mejor que puedo hacer hasta ahora (según la respuesta de Glenn Maynard) es:

try:
  ProcessFile(filePath)
except Exception as e:
  raise Exception('Failed to process file' + filePath, e), None, sys.exc_info()[2]

Esto incluye tanto los mensajes como los rastreos, pero no muestra qué excepción ocurrió en qué parte del rastreo.

EMP
fuente
3
La respuesta aceptada se está desactualizando, quizás debería considerar aceptar otra.
Aaron Hall
1
@AaronHall desafortunadamente OP no se ha visto desde 2015.
Antti Haapala

Respuestas:

136

Python 2

Es sencillo; Pase el rastreo como el tercer argumento para plantear.

import sys
class MyException(Exception): pass

try:
    raise TypeError("test")
except TypeError, e:
    raise MyException(), None, sys.exc_info()[2]

Siempre haga esto cuando detecte una excepción y vuelva a plantear otra.

Glenn Maynard
fuente
44
Gracias. Eso conserva el rastreo, pero pierde el mensaje de error de la excepción original. ¿Cómo veo ambos mensajes y ambos rastreadores?
EMP
66
raise MyException(str(e)), ..., etc.
Glenn Maynard el
23
Python 3 agrega raise E() from tby.with_traceback(...)
Dima Tisnek
3
@GlennMaynard es una pregunta bastante antigua, pero el argumento del medio raisees el valor para pasar a la excepción (en caso de que el primer argumento sea una clase de excepción y no una instancia). Así que si quieres excepciones de intercambio, en lugar de hacerlo raise MyException(str(e)), None, sys.exc_info()[2], es mejor utilizar esto: raise MyException, e.args, sys.exc_info()[2].
bgusach
8
Una forma compatible con Python2 y 3 es posible utilizando el paquete futuro: python-future.org/compatible_idioms.html#raising-exceptions Eg from future.utils import raise_y raise_(ValueError, None, sys.exc_info()[2]).
jtpereyda
239

Python 3

En python 3 puedes hacer lo siguiente:

try:
    raise MyExceptionToBeWrapped("I have twisted my ankle")

except MyExceptionToBeWrapped as e:

    raise MyWrapperException("I'm not in a good shape") from e

Esto producirá algo como esto:

   Traceback (most recent call last):
   ...
   MyExceptionToBeWrapped: ("I have twisted my ankle")

The above exception was the direct cause of the following exception:

   Traceback (most recent call last):
   ...
   MyWrapperException: ("I'm not in a good shape")
Alexei Tenitski
fuente
17
raise ... from ...de hecho, es la forma correcta de hacer esto en Python 3. Esto necesita más votos a favor.
Nakedible
NakedibleCreo que es porque desafortunadamente la mayoría de la gente todavía no usa Python 3.
Tim Ludwinski
Esto parece suceder incluso con el uso de 'from' en python 3
Steve Vermeulen
Podría ser compatible con Python 2. Espero que algún día lo haga.
Marcin Wojnarski
44
@ogrisel Puede usar el futurepaquete para lograr esto: python-future.org/compatible_idioms.html#raising-exceptions Eg from future.utils import raise_y raise_(ValueError, None, sys.exc_info()[2]).
jtpereyda
19

Python 3 tiene la raise... fromcláusula para encadenar excepciones. La respuesta de Glenn es excelente para Python 2.7, pero solo usa el rastreo de la excepción original y descarta el mensaje de error y otros detalles. Estos son algunos ejemplos en Python 2.7 que agregan información de contexto desde el alcance actual al mensaje de error de la excepción original, pero mantienen intactos otros detalles.

Tipo de excepción conocido

try:
    sock_common = xmlrpclib.ServerProxy(rpc_url+'/common')
    self.user_id = sock_common.login(self.dbname, username, self.pwd)
except IOError:
    _, ex, traceback = sys.exc_info()
    message = "Connecting to '%s': %s." % (config['connection'],
                                           ex.strerror)
    raise IOError, (ex.errno, message), traceback

Ese sabor de raisedeclaración toma el tipo de excepción como la primera expresión, los argumentos del constructor de la clase de excepción en una tupla como la segunda expresión y el rastreo como la tercera expresión. Si está ejecutando antes de Python 2.2, vea las advertencias en sys.exc_info().

Cualquier tipo de excepción

Aquí hay otro ejemplo que tiene un propósito más general si no sabe qué tipo de excepciones podría tener que atrapar su código. La desventaja es que pierde el tipo de excepción y solo genera un RuntimeError. Tienes que importar el tracebackmódulo.

except Exception:
    extype, ex, tb = sys.exc_info()
    formatted = traceback.format_exception_only(extype, ex)[-1]
    message = "Importing row %d, %s" % (rownum, formatted)
    raise RuntimeError, message, tb

Modificar el mensaje

Aquí hay otra opción si el tipo de excepción le permitirá agregarle contexto. Puede modificar el mensaje de la excepción y luego volver a subirlo.

import subprocess

try:
    final_args = ['lsx', '/home']
    s = subprocess.check_output(final_args)
except OSError as ex:
    ex.strerror += ' for command {}'.format(final_args)
    raise

Eso genera el siguiente seguimiento de pila:

Traceback (most recent call last):
  File "/mnt/data/don/workspace/scratch/scratch.py", line 5, in <module>
    s = subprocess.check_output(final_args)
  File "/usr/lib/python2.7/subprocess.py", line 566, in check_output
    process = Popen(stdout=PIPE, *popenargs, **kwargs)
  File "/usr/lib/python2.7/subprocess.py", line 710, in __init__
    errread, errwrite)
  File "/usr/lib/python2.7/subprocess.py", line 1327, in _execute_child
    raise child_exception
OSError: [Errno 2] No such file or directory for command ['lsx', '/home']

Puede ver que muestra la línea donde check_output()se llamó, pero el mensaje de excepción ahora incluye la línea de comando.

Don Kirkby
fuente
1
¿De dónde viene el ex.strerrorvino? No puedo encontrar ningún éxito relevante para eso en los documentos de Python. ¿No debería ser str(ex)?
Henrik Heimbuerger
1
IOErrorse deriva de EnvironmentError@hheimbuerger, que proporciona los atributos errornoy strerror.
Don Kirkby
¿Cómo envolvería un arbitrario Error, por ejemplo, ValueError, en una RuntimeErrorcaptura Exception? Si reproduzco su respuesta para este caso, se pierde el seguimiento de la pila.
Karl Richter
No estoy seguro de lo que estás preguntando, @karl. ¿Puedes publicar una muestra en una nueva pregunta y luego vincularla desde aquí?
Don Kirkby
Edité mi duplicado de la pregunta del OP en stackoverflow.com/questions/23157766/... con una aclaración teniendo en cuenta su respuesta directamente. Deberíamos discutir allí :)
Karl Richter
12

En Python 3.x :

raise Exception('Failed to process file ' + filePath).with_traceback(e.__traceback__)

o simplemente

except Exception:
    raise MyException()

que se propagará MyExceptionpero imprimirá ambas excepciones si no se maneja.

En Python 2.x :

raise Exception, 'Failed to process file ' + filePath, e

Puede evitar imprimir ambas excepciones eliminando el __context__atributo. Aquí escribo un administrador de contexto que lo usa para detectar y cambiar su excepción sobre la marcha: (consulte http://docs.python.org/3.1/library/stdtypes.html para ampliar cómo funcionan)

try: # Wrap the whole program into the block that will kill __context__.

    class Catcher(Exception):
        '''This context manager reraises an exception under a different name.'''

        def __init__(self, name):
            super().__init__('Failed to process code in {!r}'.format(name))

        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_val, exc_tb):
            if exc_type is not None:
                self.__traceback__ = exc_tb
                raise self

    ...


    with Catcher('class definition'):
        class a:
            def spam(self):
                # not really pass, but you get the idea
                pass

            lut = [1,
                   3,
                   17,
                   [12,34],
                   5,
                   _spam]


        assert a().lut[-1] == a.spam

    ...


except Catcher as e:
    e.__context__ = None
    raise
ilya n.
fuente
44
TypeError: raise: arg 3 debe ser un rastreo o Ninguno
Glenn Maynard
Lo siento, cometí un error, de alguna manera pensé que también acepta excepciones y obtiene su atributo de rastreo automáticamente. Según docs.python.org/3.1/reference/… , esto debería ser e .__ traceback__
ilya n.
1
@ilyan .: ¡Python 2 no tiene e.__traceback__atributo!
Jan Hudec el
5

No creo que pueda hacer esto en Python 2.x, pero algo similar a esta funcionalidad es parte de Python 3. De PEP 3134 :

En la implementación actual de Python, las excepciones se componen de tres partes: el tipo, el valor y el rastreo. El módulo 'sys' expone la excepción actual en tres variables paralelas, exc_type, exc_value y exc_traceback, la función sys.exc_info () devuelve una tupla de estas tres partes, y la declaración 'raise' tiene una forma de tres argumentos que acepta Estas tres partes. La manipulación de excepciones a menudo requiere pasar estas tres cosas en paralelo, lo que puede ser tedioso y propenso a errores. Además, la declaración 'excepto' solo puede proporcionar acceso al valor, no el rastreo. Agregar el atributo ' traceback ' a los valores de excepción hace que toda la información de excepción sea accesible desde un solo lugar.

Comparación con C #:

Las excepciones en C # contienen una propiedad 'InnerException' de solo lectura que puede apuntar a otra excepción. Su documentación [10] dice que "cuando se lanza una excepción X como resultado directo de una excepción anterior Y, la propiedad InnerException de X debe contener una referencia a Y". La VM no establece esta propiedad automáticamente; más bien, todos los constructores de excepciones toman un argumento opcional 'innerException' para establecerlo explícitamente. El atributo ' causa ' cumple el mismo propósito que InnerException, pero este PEP propone una nueva forma de 'elevar' en lugar de extender los constructores de todas las excepciones. C # también proporciona un método GetBaseException que salta directamente al final de la cadena InnerException;

Tenga en cuenta también que Java, Ruby y Perl 5 tampoco admiten este tipo de cosas. Citando de nuevo:

En cuanto a otros lenguajes, Java y Ruby descartan la excepción original cuando ocurre otra excepción en una cláusula 'catch' / 'rescue' o 'finally' / 'sure' Perl 5 carece de un manejo de excepciones estructurado incorporado. Para Perl 6, el RFC número 88 [9] propone un mecanismo de excepción que retiene implícitamente las excepciones encadenadas en una matriz llamada @@.

ire_and_curses
fuente
Pero, por supuesto, en Perl5 puede decir "confesar qq {OH NOES! $ @}" Y no perder el rastro de la pila de la otra excepción. O puede implementar su propio tipo que conserva la excepción.
jrockway
4

Para una máxima compatibilidad entre Python 2 y 3, puede usar raise_fromen la sixbiblioteca. https://six.readthedocs.io/#six.raise_from . Aquí está su ejemplo (ligeramente modificado para mayor claridad):

import six

try:
  ProcessFile(filePath)
except Exception as e:
  six.raise_from(IOError('Failed to process file ' + repr(filePath)), e)
LexH
fuente
3

Puede usar mi clase CausedException para encadenar excepciones en Python 2.x (e incluso en Python 3 puede ser útil en caso de que desee dar más de una excepción capturada como causa de una excepción recién planteada). Puede que esto te ayude.

Alfe
fuente
2

¿Tal vez podrías obtener la información relevante y pasarla? Estoy pensando en algo como:

import traceback
import sys
import StringIO

class ApplicationError:
    def __init__(self, value, e):
        s = StringIO.StringIO()
        traceback.print_exc(file=s)
        self.value = (value, s.getvalue())

    def __str__(self):
        return repr(self.value)

try:
    try:
        a = 1/0
    except Exception, e:
        raise ApplicationError("Failed to process file", e)
except Exception, e:
    print e
brool
fuente
2

Asumiendo:

  • necesita una solución, que funcione para Python 2 (para Python 3 puro, vea la raise ... fromsolución)
  • solo quiero enriquecer el mensaje de error, por ejemplo, proporcionar un contexto adicional
  • necesita el seguimiento completo de la pila

puede usar una solución simple de los documentos https://docs.python.org/3/tutorial/errors.html#raising-exceptions :

try:
    raise NameError('HiThere')
except NameError:
    print 'An exception flew by!' # print or log, provide details about context
    raise # reraise the original exception, keeping full stack trace

La salida:

An exception flew by!
Traceback (most recent call last):
  File "<stdin>", line 2, in ?
NameError: HiThere

Parece que la pieza clave es la palabra clave "subir" simplificada que se encuentra sola. Eso volverá a aumentar la Excepción en el bloque excepto.

Un dulce
fuente
¡Esta es la solución compatible con Python 2 y 3! ¡Gracias!
Andy Chase
Creo que la idea era plantear un tipo diferente de excepción.
Tim Ludwinski
2
Esta no es una cadena de excepciones anidadas, solo plantea una excepción
Karl Richter
¡Esta es la mejor solución de Python 2, si solo necesita enriquecer el mensaje de excepción y tener el seguimiento completo de la pila!
geekQ
¿Cuál es la diferencia entre el uso de aumento y aumento de
variable de