¿Cómo volver a generar una excepción en bloques try / except anidados?

107

Sé que si quiero volver a generar una excepción, lo uso raisesin argumentos en el exceptbloque respectivo . Pero dada una expresión anidada como

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # I'd like to raise the SomeError as if plan_B()
                 # didn't raise the AlsoFailsError

¿Cómo puedo volver a subir el SomeErrorsin romper el rastro de la pila? raisesolo en este caso volvería a plantear el más reciente AlsoFailsError. ¿O cómo podría refactorizar mi código para evitar este problema?

Tobias Kienzler
fuente
2
¿Ha intentado poner plan_Botra función que regrese Truecon éxito y Falsecon excepción? Entonces el exceptbloque exterior podría serif not try_plan_B(): raise
Drew McGowen
@DrewMcGowen Desafortunadamente, el caso más realista es que esto está dentro de una función que acepta objetos arbitrarios arge intentaría llamar, lo arg.plan_B()que podría generar un AttributeErrorarg
error
Eche un vistazo al módulo de rastreo
Paco
@Paco Gracias, lo haré (aunque una respuesta ya muestra una forma más simple)
Tobias Kienzler
@DrewMcGowen Escribí una respuesta basada en su comentario , que parece menos pitónico que la respuesta del usuario4815162342 . Pero eso se debe a que quiero tener también un valor de retorno y permitir plan_Bgenerar excepciones
Tobias Kienzler

Respuestas:

128

A partir de Python 3, el rastreo se almacena en la excepción, por lo que un simple raise ehará lo correcto (en su mayoría):

try:
    something()
except SomeError as e:
    try:
        plan_B()
    except AlsoFailsError:
        raise e  # or raise e from None - see below

El rastreo producido incluirá un aviso adicional que SomeErrorocurrió durante la manipulación AlsoFailsError(por raise eestar dentro except AlsoFailsError). Esto es engañoso porque lo que realmente sucedió es al revés: lo encontramos AlsoFailsErrory lo manejamos mientras intentamos recuperarnos SomeError. Para obtener un rastreo que no incluye AlsoFailsError, reemplace raise econ raise e from None.

En Python 2, almacenaría el tipo de excepción, el valor y el rastreo en variables locales y usaría la forma de tres argumentos deraise :

try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        raise t, v, tb
usuario4815162342
fuente
Perfecto, eso es lo que también encontré aquí , ¡gracias! Aunque existe la sugerencia es raise self.exc_info[1], None, self.exc_info[2]después self.exc_info = sys.exc_info()- poner [1]a la primera posición por alguna razón
Tobias Kienzler
3
@TobiasKienzler raise t, None, tbperderá el valor de la excepción y obligará raisea volver a crear una instancia del tipo, dándole un valor de excepción menos específico (o simplemente incorrecto). Por ejemplo, si la excepción generada es KeyError("some-key"), simplemente volverá a generar KeyError()y omitirá la clave exacta que falta en el rastreo.
user4815162342
3
@TobiasKienzler Aún debería ser posible expresar eso en Python 3 como raise v.with_traceback(tb). (Su comentario incluso dice lo mismo, excepto que propone volver a crear una instancia del valor.)
user4815162342
2
Además, la advertencia roja de no almacenar sys.exc_info()en una variable local tenía sentido antes de Python 2.0 (lanzado hace 13 años), pero hoy roza el ridículo. El Python moderno sería casi inútil sin el recopilador de ciclos, ya que cada biblioteca de Python no trivial crea ciclos sin pausa y depende de su correcta limpieza.
user4815162342
1
@ user4815162342 Puede eliminar el error anidado "ocurrió otro error" escribiendo "raise e from None".
Matthias Urlichs
19

Incluso si la solución aceptada es correcta, es bueno señalar la biblioteca Six que tiene una solución Python 2 + 3, usando six.reraise.

seis. reraise ( exc_type , exc_value , exc_traceback = None)

Vuelva a generar una excepción, posiblemente con un rastreo diferente. [...]

Entonces, puedes escribir:

import six


try:
    something()
except SomeError:
    t, v, tb = sys.exc_info()
    try:
        plan_B()
    except AlsoFailsError:
        six.reraise(t, v, tb)
Laurent LAPORTE
fuente
1
Buen punto: hablando de Six, también puede usar six.raise_fromsi desea incluir información que plan_B()también falló.
Tobias Kienzler
1
@TobiasKienzler: Creo que es un uso diferente: six.raise_fromcuando creas una nueva excepción que está vinculada a una anterior, no vuelves a subir , por lo que el rastreo es diferente.
Laurent LAPORTE
1
Mi punto exactamente: si te reraiseda la impresión de que solo something()arrojó SomeError, si raise_fromtambién sabes que esto causó plan_B()que se ejecutara, pero arrojando el AlsoFailsError. Entonces depende del caso de uso. Creo raise_fromque facilitará la depuración
Tobias Kienzler
9

Según la sugerencia de Drew McGowen , pero teniendo en cuenta un caso general (donde hay un valor de retorno s), aquí hay una alternativa a la respuesta del usuario4815162342 :

try:
    s = something()
except SomeError as e:
    def wrapped_plan_B():
        try:
            return False, plan_B()
        except:
            return True, None
    failed, s = wrapped_plan_B()
    if failed:
        raise
Tobias Kienzler
fuente
1
Lo bueno de este enfoque es que funciona sin cambios en Python 2 y 3.
user4815162342
2
@ user4815162342 Buen punto :) Aunque mientras tanto en Python3 lo consideraría raise from, por lo que el seguimiento de la pila también me permitiría ver que el plan B falló. Que se puede emular en Python 2 por cierto.
Tobias Kienzler
5

Python 3.5+ adjunta la información de rastreo al error de todos modos, por lo que ya no es necesario guardarlo por separado.

>>> def f():
...   try:
...     raise SyntaxError
...   except Exception as e:
...     err = e
...     try:
...       raise AttributeError
...     except Exception as e1:
...       raise err from None
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in f
  File "<stdin>", line 3, in f
SyntaxError: None
>>> 
Matthias Urlichs
fuente
2
La pregunta es sobre otra excepción que ocurre durante el except. Pero tienes razón, cuando reemplazo err = epor, digamos, raise AttributeErrorobtienes primero el SyntaxErrorseguimiento de la pila, seguido de ay el seguimiento de During handling of the above exception, another exception occurred:la AttributeErrorpila. Es bueno saberlo, aunque desafortunadamente no se puede confiar en que se instale 3.5+. PD: ff verstehen nicht-Deutsche vermutlich nicht;)
Tobias Kienzler
Bien, cambié el ejemplo para generar otra excepción, que (como se pidió en la pregunta original) se ignora cuando vuelvo a generar la primera.
Matthias Urlichs
3
@TobiasKienzler 3.5+ (al que lo cambié) parece ser un formato reconocido mundialmente. Was denkst du? ;)
linusg
@linusg De acuerdo :)
Tobias Kienzler