Vuelva a generar la excepción con un tipo y mensaje diferente, conservando la información existente

139

Estoy escribiendo un módulo y quiero tener una jerarquía de excepción unificada para las excepciones que puede generar (por ejemplo, heredar de una FooErrorclase abstracta para todas las fooexcepciones específicas del módulo). Esto permite a los usuarios del módulo capturar esas excepciones particulares y manejarlas de manera distinta, si es necesario. Pero muchas de las excepciones generadas desde el módulo se generan debido a alguna otra excepción; por ejemplo, fallar en alguna tarea debido a un error OSEr en un archivo.

Lo que necesito es "envolver" la excepción detectada de manera que tenga un tipo y mensaje diferente , de modo que la información esté disponible más arriba en la jerarquía de propagación por lo que detecte la excepción. Pero no quiero perder el tipo existente, el mensaje y el seguimiento de la pila; Esa es toda la información útil para alguien que intenta depurar el problema. Un controlador de excepciones de nivel superior no es bueno, ya que estoy tratando de decorar la excepción antes de que suba por la pila de propagación, y el controlador de nivel superior es demasiado tarde.

Esto se resuelve en parte derivando los footipos de excepción específicos de mi módulo del tipo existente (por ejemplo class FooPermissionError(OSError, FooError)), pero eso no hace que sea más fácil envolver la instancia de excepción existente en un nuevo tipo, ni modificar el mensaje.

El PEP 3134 de Python "Encadenamiento de excepciones y seguimientos integrados" analiza un cambio aceptado en Python 3.0 para "encadenar" objetos de excepción, para indicar que se produjo una nueva excepción durante el manejo de una excepción existente.

Lo que intento hacer está relacionado: también necesito que funcione en versiones anteriores de Python, y no lo necesito para encadenar, sino solo para el polimorfismo. ¿Cuál es la forma correcta de hacer esto?

nariz grande
fuente
Las excepciones ya son completamente polimórficas: todas son subclases de excepción. ¿Que estás tratando de hacer? "Mensaje diferente" es bastante trivial con un controlador de excepciones de nivel superior. ¿Por qué estás cambiando la clase?
S.Lott
Como se explica en la pregunta (ahora, gracias por tu comentario): estoy tratando de decorar una excepción que he detectado, para que pueda propagarse más con más información pero sin perder ninguna. Un controlador de nivel superior es demasiado tarde.
bignose
Eche un vistazo a mi clase CausedException que puede hacer lo que quiera en Python 2.x. También en Python 3 puede ser útil en caso de que desee dar más de una excepción original como causa de su excepción. Quizás se ajuste a tus necesidades.
Alfe
Para python-2, hago algo similar a @DevinJeanpierre pero solo agrego un nuevo mensaje de cadena: except Exception as e-> raise type(e), type(e)(e.message + custom_message), sys.exc_info()[2]-> esta solución es de otra pregunta SO . Esto no es bonito sino funcional.
Trevor Boyd Smith

Respuestas:

197

Python 3 introdujo el encadenamiento de excepciones (como se describe en PEP 3134 ). Esto permite, al generar una excepción, citar una excepción existente como la "causa":

try:
    frobnicate()
except KeyError as exc:
    raise ValueError("Bad grape") from exc

La excepción capturada se convierte en parte de (es la "causa de") la nueva excepción, y está disponible para cualquier código que capture la nueva excepción.

Al usar esta función, __cause__se establece el atributo. El controlador de excepciones incorporado también sabe cómo informar la "causa" y el "contexto" de la excepción junto con el rastreo.


En Python 2 , parece que este caso de uso no tiene una buena respuesta (como lo describen Ian Bicking y Ned Batchelder ). Gorrón.

nariz grande
fuente
44
¿Ian Bicking no describe mi solución? Lamento haber dado una respuesta tan graciosa, pero es extraño que esta haya sido aceptada.
Devin Jeanpierre
1
@bignose Obtuviste mi punto no solo de tener razón, sino del uso de "frobnicate" :)
David M.
55
El encadenamiento de excepciones es en realidad el comportamiento predeterminado ahora, de hecho, es todo lo contrario, suprimiendo la primera excepción que requiere trabajo, ver PEP 409 python.org/dev/peps/pep-0409
Chris_Rands
1
¿Cómo lograrías esto en Python 2?
selotape
1
Parece funcionar bien (python 2.7)try: return 2 / 0 except ZeroDivisionError as e: raise ValueError(e)
alex
37

Puede usar sys.exc_info () para obtener el rastreo y generar su nueva excepción con dicho rastreo (como menciona el PEP). Si desea conservar el tipo y el mensaje antiguos, puede hacerlo con la excepción, pero eso solo es útil si lo que detecta su excepción lo busca.

Por ejemplo

import sys

def failure():
    try: 1/0
    except ZeroDivisionError, e:
        type, value, traceback = sys.exc_info()
        raise ValueError, ("You did something wrong!", type, value), traceback

Por supuesto, esto realmente no es tan útil. Si lo fuera, no necesitaríamos esa PEP. No recomendaría hacerlo.

Devin Jeanpierre
fuente
Devin, guardas una referencia al rastreo allí, ¿no deberías eliminar explícitamente esa referencia?
Arafangion
2
No almacené nada, dejé el rastreo como una variable local que presumiblemente queda fuera de alcance. Sí, es concebible que no sea así, pero si plantea excepciones como esa en el ámbito global en lugar de dentro de las funciones, tiene problemas más grandes. Si su queja es solo que podría ejecutarse en un ámbito global, la solución adecuada no es agregar repeticiones irrelevantes que deben explicarse y no son relevantes para el 99% de los usos, sino reescribir la solución para que no exista tal cosa. es necesario mientras hace que parezca que nada es diferente, como lo he hecho ahora.
Devin Jeanpierre
44
Arafangion puede estar refiriéndose a una advertencia en la documentación de Python parasys.exc_info() @Devin. Dice: "Asignar el valor de retorno de rastreo a una variable local en una función que maneja una excepción causará una referencia circular". Sin embargo, una nota siguiente dice que desde Python 2.2, el ciclo se puede limpiar, pero es más eficiente simplemente evitarlo.
Don Kirkby el
55
Más detalles sobre diferentes maneras de volver a generar excepciones en Python de dos pythonistas iluminados: Ian Bicking y Ned Batchelder
Rodrigue
11

Puede crear su propio tipo de excepción que se extiende a cualquier excepción que haya detectado.

class NewException(CaughtException):
    def __init__(self, caught):
        self.caught = caught

try:
    ...
except CaughtException as e:
    ...
    raise NewException(e)

Pero la mayoría de las veces, creo que sería más sencillo detectar la excepción, manejarla y raisela excepción original (y preservar el rastreo) o raise NewException(). Si estuviera llamando a su código, y recibí una de sus excepciones personalizadas, esperaría que su código ya haya manejado cualquier excepción que haya tenido que atrapar. Por lo tanto, no necesito acceder a mí mismo.

Editar: Encontré este análisis de formas de lanzar su propia excepción y mantener la excepción original. No hay soluciones bonitas.

Nikhil Chelliah
fuente
1
El caso de uso que he descrito no es para manejar la excepción; se trata específicamente de no manejarlo, sino de agregar información adicional (una clase adicional y un nuevo mensaje) para que pueda manejarse más arriba en la pila de llamadas.
bignose
2

También descubrí que muchas veces necesito un "ajuste" a los errores generados.

Esto incluía tanto el alcance de una función como, a veces, solo algunas líneas dentro de una función.

Creado un envoltorio que se utilizará una decoratory context manager:


Implementación

import inspect
from contextlib import contextmanager, ContextDecorator
import functools    

class wrap_exceptions(ContextDecorator):
    def __init__(self, wrapper_exc, *wrapped_exc):
        self.wrapper_exc = wrapper_exc
        self.wrapped_exc = wrapped_exc

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        if not exc_type:
            return
        try:
            raise exc_val
        except self.wrapped_exc:
            raise self.wrapper_exc from exc_val

    def __gen_wrapper(self, f, *args, **kwargs):
        with self:
            for res in f(*args, **kwargs):
                yield res

    def __call__(self, f):
        @functools.wraps(f)
        def wrapper(*args, **kw):
            with self:
                if inspect.isgeneratorfunction(f):
                    return self.__gen_wrapper(f, *args, **kw)
                else:
                    return f(*args, **kw)
        return wrapper

Ejemplos de uso

decorador

@wrap_exceptions(MyError, IndexError)
def do():
   pass

cuando llame al dométodo, no se preocupe IndexError, soloMyError

try:
   do()
except MyError as my_err:
   pass # handle error 

gestor de contexto

def do2():
   print('do2')
   with wrap_exceptions(MyError, IndexError):
       do()

adentro do2, en elcontext manager , si IndexErrorse eleva, se envolverá y se elevaráMyError

Aaron_ab
fuente
1
Explique qué hará el "envoltorio" a la excepción original. ¿Cuál es el propósito de su código y qué comportamiento permite?
alexis
@alexis - agregó algunos ejemplos, espero que ayude
Aaron_ab
-2

La solución más sencilla para sus necesidades debería ser esta:

try:
     upload(file_id)
except Exception as upload_error:
     error_msg = "Your upload failed! File: " + file_id
     raise RuntimeError(error_msg, upload_error)

De esta forma, luego puede imprimir su mensaje y el error específico arrojado por la función de carga

funny_dev
fuente
1
Eso atrapa y luego arroja el objeto de excepción, así que no, no satisface las necesidades de la pregunta. La pregunta pregunta cómo mantener la excepción existente y permitir esa misma excepción, con toda la información útil que contiene, para continuar propagándose en la pila.
bignose