¿Forma correcta de declarar excepciones personalizadas en Python moderno?

1292

¿Cuál es la forma correcta de declarar clases de excepción personalizadas en Python moderno? Mi objetivo principal es seguir cualquier estándar que tengan otras clases de excepción, de modo que (por ejemplo) cualquier cadena adicional que incluya en la excepción se imprima mediante cualquier herramienta que haya detectado la excepción.

Por "Python moderno" me refiero a algo que se ejecutará en Python 2.5 pero será "correcto" para la forma de hacer las cosas Python 2.6 y Python 3. *. Y por "personalizado" me refiero a un objeto de excepción que puede incluir datos adicionales sobre la causa del error: una cadena, tal vez también algún otro objeto arbitrario relevante para la excepción.

Me tropecé con la siguiente advertencia de desaprobación en Python 2.6.2:

>>> class MyError(Exception):
...     def __init__(self, message):
...         self.message = message
... 
>>> MyError("foo")
_sandbox.py:3: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6

Parece una locura que BaseExceptiontenga un significado especial para los atributos nombrados message. Yo recojo de PEP-352 deduzco que ese atributo tenía un significado especial en 2.5 que están tratando de desaprobar, así que supongo que ese nombre (y ese solo) ahora está prohibido. Ugh

También soy consciente de que Exceptiontiene algún parámetro mágicoargs , pero nunca he sabido cómo usarlo. Tampoco estoy seguro de que sea la forma correcta de hacer las cosas en el futuro; Gran parte de la discusión que encontré en línea sugirió que estaban tratando de eliminar los argumentos en Python 3.

Actualización: dos respuestas han sugerido anular __init__, y __str__/ __unicode__/ __repr__. Parece mucho escribir, ¿es necesario?

Nelson
fuente

Respuestas:

1323

Tal vez me perdí la pregunta, pero por qué no:

class MyException(Exception):
    pass

Editar: para anular algo (o pasar argumentos adicionales), haga esto:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

De esa manera, podría pasar mensajes de error de error al segundo parámetro y acceder a él más tarde con e.errors


Actualización de Python 3: en Python 3+, puede usar este uso un poco más compacto de super():

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super().__init__(message)

        # Now for your custom code...
        self.errors = errors
gahooa
fuente
35
Sin embargo, una excepción definida como esta no sería seleccionable; vea la discusión aquí stackoverflow.com/questions/16244923/…
jiakai
87
@jiakai significa "picklable". :-)
Robino
1
Siguiendo la documentación de python para excepciones definidas por el usuario, los nombres que se mencionan en la función __init__ son incorrectos. En lugar de (sí mismo, mensaje, error) es (sí mismo, expresión, mensaje). La expresión de atributo es la expresión de entrada en la que se produjo el error y el mensaje es una explicación del error.
Ddleon
2
Eso es un malentendido, @ddleon. El ejemplo en los documentos a los que se refiere es para un caso de uso particular. No hay importancia para el nombre de los argumentos del constructor de la subclase (ni su número).
asthasr
498

Con modernas excepciones Python, que no es necesario para el abuso .message, o anulación .__str__()o .__repr__()o nada de eso. Si todo lo que desea es un mensaje informativo cuando se genera su excepción, haga lo siguiente:

class MyException(Exception):
    pass

raise MyException("My hovercraft is full of eels")

Eso dará un rastreo que termina en MyException: My hovercraft is full of eels.

Si desea más flexibilidad de la excepción, puede pasar un diccionario como argumento:

raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})

Sin embargo, llegar a esos detalles en un exceptbloque es un poco más complicado. Los detalles se almacenan en el argsatributo, que es una lista. Tendría que hacer algo como esto:

try:
    raise MyException({"message":"My hovercraft is full of animals", "animal":"eels"})
except MyException as e:
    details = e.args[0]
    print(details["animal"])

Todavía es posible pasar múltiples elementos a la excepción y acceder a ellos a través de índices de tupla, pero esto es altamente desaconsejado (e incluso fue pensado para la desvalorización hace un tiempo). Si necesita más que una sola información y el método anterior no es suficiente para usted, entonces debe subclasificar Exceptioncomo se describe en el tutorial .

class MyError(Exception):
    def __init__(self, message, animal):
        self.message = message
        self.animal = animal
    def __str__(self):
        return self.message
frnknstn
fuente
2
"pero esto quedará en desuso en el futuro", ¿esto todavía está destinado al desuso? Python 3.7 todavía parece aceptar felizmente Exception(foo, bar, qux).
mtraceur
No se ha visto ningún trabajo reciente para eliminarlo desde que fracasó el último intento debido al dolor de la transición, pero ese uso aún se desaconseja. Actualizaré mi respuesta para reflejar eso.
frnknstn
@frnknstn, ¿por qué se desalienta? Parece un buen modismo para mí.
neves
2
@neves para empezar, el uso de tuplas para almacenar información de excepción no tiene ningún beneficio sobre el uso de un diccionario para hacer lo mismo. Si está interesado en el razonamiento detrás de los cambios de excepción, eche un vistazo a PEP352
frnknstn
La sección relevante de PEP352 es "Ideas retractadas " .
liberforce
196

"¿Forma correcta de declarar excepciones personalizadas en Python moderno?"

Esto está bien, a menos que su excepción sea realmente un tipo de excepción más específica:

class MyException(Exception):
    pass

O mejor (tal vez perfecto), en lugar de passdar una cadena de documentación:

class MyException(Exception):
    """Raise for my specific kind of exception"""

Subclases Excepción Subclases

De los documentos

Exception

Todas las excepciones integradas que no salen del sistema se derivan de esta clase. Todas las excepciones definidas por el usuario también deben derivarse de esta clase.

Eso significa que si su excepción es un tipo de excepción más específica, subclase esa excepción en lugar de la genérica Exception(y el resultado será el que aún deriva Exceptioncomo lo recomiendan los documentos). Además, al menos puede proporcionar una cadena de documentación (y no verse obligado a usar la passpalabra clave):

class MyAppValueError(ValueError):
    '''Raise when my specific value is wrong'''

Establezca atributos que cree usted mismo con una costumbre __init__. Evite pasar un dict como argumento posicional, los futuros usuarios de su código se lo agradecerán. Si usa el atributo de mensaje en desuso, asignándolo usted mismo evitará un DeprecationWarning:

class MyAppValueError(ValueError):
    '''Raise when a specific subset of values in context of app is wrong'''
    def __init__(self, message, foo, *args):
        self.message = message # without this you may get DeprecationWarning
        # Special attribute you desire with your Error, 
        # perhaps the value that caused the error?:
        self.foo = foo         
        # allow users initialize misc. arguments as any other builtin Error
        super(MyAppValueError, self).__init__(message, foo, *args) 

En realidad no hay necesidad de escribir su propio __str__o __repr__. Los incorporados son muy agradables y su herencia cooperativa asegura que lo use.

Crítica de la respuesta superior

Tal vez me perdí la pregunta, pero por qué no:

class MyException(Exception):
    pass

Una vez más, el problema con lo anterior es que para atraparlo, tendrá que nombrarlo específicamente (importándolo si se creó en otro lugar) o atrapar Exception (pero probablemente no esté preparado para manejar todo tipo de Excepciones, y solo debe detectar las excepciones que está preparado para manejar). Crítica similar a la siguiente, pero además esa no es la forma de inicializar a través de super, y obtendrá un DeprecationWarningsi accede al atributo de mensaje:

Editar: para anular algo (o pasar argumentos adicionales), haga esto:

class ValidationError(Exception):
    def __init__(self, message, errors):

        # Call the base class constructor with the parameters it needs
        super(ValidationError, self).__init__(message)

        # Now for your custom code...
        self.errors = errors

De esa forma, puede pasar mensajes de error de error al segundo parámetro y acceder a él más tarde con e.errors

También requiere que se pasen exactamente dos argumentos (aparte del self.) Ni más ni menos. Esa es una restricción interesante que los futuros usuarios pueden no apreciar.

Para ser directo, viola la sustituibilidad de Liskov .

Demostraré ambos errores:

>>> ValidationError('foo', 'bar', 'baz').message

Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    ValidationError('foo', 'bar', 'baz').message
TypeError: __init__() takes exactly 3 arguments (4 given)

>>> ValidationError('foo', 'bar').message
__main__:1: DeprecationWarning: BaseException.message has been deprecated as of Python 2.6
'foo'

Comparado con:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'
Aaron Hall
fuente
2
Hola desde 2018! BaseException.messagese ha ido en Python 3, por lo que la crítica solo es válida para versiones antiguas, ¿verdad?
Kos
55
@Kos La crítica sobre la sustituibilidad de Liskov sigue siendo válida. La semántica del primer argumento como "mensaje" también es discutible, pero no creo que discuta el punto. Echaré un vistazo más cuando tenga más tiempo libre.
Aaron Hall
1
FWIW, para Python 3 (al menos para 3.6+), uno redefiniría el __str__método en MyAppValueErrorlugar de confiar en el messageatributo
Jacquot
1
@AaronHall ¿Podría ampliar el beneficio de subclasificar ValueError en lugar de Exception? Usted declara que esto es lo que significan los documentos, pero una lectura directa no admite esa interpretación y en el Tutorial de Python bajo Excepciones definidas por el usuario, claramente lo hace la elección de los usuarios: "Las excepciones generalmente deben derivarse de la clase Excepción, ya sea directa o indirectamente ". Por lo tanto, dispuesto a entender si su punto de vista es justificable, por favor.
ostergaard
1
@ostergaard No puedo responder por completo en este momento, pero en resumen, el usuario tiene la opción adicional de capturar ValueError. Esto tiene sentido si está en la categoría de Errores de valor. Si no está en la categoría de Errores de valor, argumentaría en contra de la semántica. Hay espacio para algunos matices y razonamientos por parte del programador, pero prefiero la especificidad cuando corresponde. Actualizaré mi respuesta para abordar mejor el tema pronto.
Aaron Hall
50

vea cómo funcionan las excepciones de forma predeterminada si se utilizan uno frente a más atributos (se omiten las trazas):

>>> raise Exception('bad thing happened')
Exception: bad thing happened

>>> raise Exception('bad thing happened', 'code is broken')
Exception: ('bad thing happened', 'code is broken')

por lo que es posible que desee tener una especie de " plantilla de excepción ", que funcione como una excepción en sí misma, de manera compatible:

>>> nastyerr = NastyError('bad thing happened')
>>> raise nastyerr
NastyError: bad thing happened

>>> raise nastyerr()
NastyError: bad thing happened

>>> raise nastyerr('code is broken')
NastyError: ('bad thing happened', 'code is broken')

Esto se puede hacer fácilmente con esta subclase

class ExceptionTemplate(Exception):
    def __call__(self, *args):
        return self.__class__(*(self.args + args))
# ...
class NastyError(ExceptionTemplate): pass

y si no le gusta esa representación de tipo tupla predeterminada, simplemente agregue el __str__método a la ExceptionTemplateclase, como:

    # ...
    def __str__(self):
        return ': '.join(self.args)

y tendrás

>>> raise nastyerr('code is broken')
NastyError: bad thing happened: code is broken
mykhal
fuente
32

A partir de Python 3.8 (2018, https://docs.python.org/dev/whatsnew/3.8.html ), el método recomendado sigue siendo:

class CustomExceptionName(Exception):
    """Exception raised when very uncommon things happen"""
    pass

¡No olvide documentar, por qué es necesaria una excepción personalizada!

Si lo necesita, este es el camino a seguir para excepciones con más datos:

class CustomExceptionName(Exception):
    """Still an exception raised when uncommon things happen"""
    def __init__(self, message, payload=None):
        self.message = message
        self.payload = payload # you could add more args
    def __str__(self):
        return str(self.message) # __str__() obviously expects a string to be returned, so make sure not to send any other data types

y traerlos como:

try:
    raise CustomExceptionName("Very bad mistake.", "Forgot upgrading from Python 1")
except CustomExceptionName as error:
    print(str(error)) # Very bad mistake
    print("Detail: {}".format(error.payload)) # Detail: Forgot upgrading from Python 1

payload=NoneEs importante hacer que se pueda conservar en vinagre. Antes de tirarlo, tienes que llamar error.__reduce__(). La carga funcionará como se espera.

Tal vez debería investigar para encontrar una solución usando la returndeclaración de pitones si necesita transferir muchos datos a alguna estructura externa. Esto parece ser más claro / más pitónico para mí. Las excepciones avanzadas se usan mucho en Java, lo que a veces puede ser molesto, cuando se usa un marco y se tiene que detectar todos los errores posibles.

fama
fuente
1
Por lo menos, los documentos actuales indican que esta es la forma de hacerlo (al menos sin el __str__) en lugar de otras respuestas que usan super().__init__(...)... Es una pena que anule __str__y __repr__probablemente sea necesario solo para una mejor serialización "predeterminada".
kevlarr
2
Pregunta honesta: ¿Por qué es importante que las excepciones sean aptas para pickle? ¿Cuáles son los casos de uso para las excepciones de descarga y carga?
Roel Schroeven
1
@RoelSchroeven: tuve que paralelizar el código una vez. Funcionó bien con un solo proceso, pero los aspectos de algunas de sus clases no eran serializables (la función lambda se pasaba como objetos). Me llevó algo de tiempo resolverlo y arreglarlo. Lo que significa que alguien más tarde puede terminar necesitando que su código se serialice, no pueda hacerlo y tenga que desenterrar por qué ... Mi problema no fueron errores imperdonables, pero puedo ver que causa problemas similares.
logicOnAbstraction
17

Debe anular __repr__o __unicode__métodos en lugar de usar el mensaje, los argumentos que proporcione cuando construya la excepción estarán en el argsatributo del objeto de excepción.

M. Utku ALTINKAYA
fuente
7

No, "mensaje" no está prohibido. Simplemente está en desuso. Su aplicación funcionará bien con el uso del mensaje. Pero es posible que desee deshacerse del error de desaprobación, por supuesto.

Cuando crea clases de Excepción personalizadas para su aplicación, muchas de ellas no se subclasifican solo de Excepción, sino de otras, como ValueError o similar. Luego hay que adaptarse a su uso de variables.

Y si tiene muchas excepciones en su aplicación, generalmente es una buena idea tener una clase base personalizada común para todas ellas, de modo que los usuarios de sus módulos puedan hacer

try:
    ...
except NelsonsExceptions:
    ...

Y en ese caso puedes hacer el __init__ and __str__ necesario allí, por lo que no tiene que repetirlo para cada excepción. Pero simplemente llamar a la variable del mensaje algo más que mensaje hace el truco.

En cualquier caso, solo necesita el __init__ or __str__si hace algo diferente de lo que hace la Excepción misma. Y porque si la desaprobación, entonces necesita ambos, o se obtiene un error. Eso no es una gran cantidad de código adicional que necesita por clase. ;)

Lennart Regebro
fuente
Es interesante que las excepciones de Django no se hereden de una base común. docs.djangoproject.com/en/2.2/_modules/django/core/exceptions ¿Tiene un buen ejemplo cuando necesita capturar todas las excepciones de una aplicación específica? (tal vez sea útil solo para algunos tipos específicos de aplicaciones).
Yaroslav Nikitenko
Encontré un buen artículo sobre este tema, julien.danjou.info/python-exceptions-guide . Creo que las Excepciones deberían subclasificarse principalmente basadas en el dominio, no en la aplicación. Cuando su aplicación se trata del protocolo HTTP, deriva de HTTPError. Cuando parte de su aplicación es TCP, deriva las excepciones de esa parte de TCPError. Pero si su aplicación abarca muchos dominios (archivos, permisos, etc.), la razón para tener una excepción MyBaseException disminuye. ¿O es para protegerse de la 'violación de capa'?
Yaroslav Nikitenko
6

Vea un muy buen artículo " La guía definitiva para las excepciones de Python ". Los principios básicos son:

  • Siempre herede de (al menos) Excepción.
  • Siempre llame BaseException.__init__con un solo argumento.
  • Al construir una biblioteca, defina una clase base que herede de Exception.
  • Proporcione detalles sobre el error.
  • Herede de los tipos de excepciones incorporados cuando tenga sentido.

También hay información sobre organización (en módulos) y excepciones de envoltura, recomiendo leer la guía.

Yaroslav Nikitenko
fuente
1
Este es un buen ejemplo de por qué en SO generalmente reviso la respuesta más votada, pero también las más recientes. Además útil, gracias.
logicOnAbstraction
1
Always call BaseException.__init__ with only one argument.Parece una restricción innecesaria, ya que en realidad acepta cualquier número de argumentos.
Eugene Yarmash
@EugeneYarmash Estoy de acuerdo, ahora no entiendo eso. No lo uso de todos modos. Tal vez debería releer el artículo y ampliar mi respuesta.
Yaroslav Nikitenko
@ EugeneYarmash Leí el artículo nuevamente. Se afirma que en caso de varios argumentos, la implementación de C llama "return PyObject_Str (self-> args)"; Significa que una cadena debería funcionar mejor que varias. ¿Lo comprobaste?
Yaroslav Nikitenko
3

Prueba este ejemplo

class InvalidInputError(Exception):
    def __init__(self, msg):
        self.msg = msg
    def __str__(self):
        return repr(self.msg)

inp = int(input("Enter a number between 1 to 10:"))
try:
    if type(inp) != int or inp not in list(range(1,11)):
        raise InvalidInputError
except InvalidInputError:
    print("Invalid input entered")
Omkaar.K
fuente
1

Para definir sus propias excepciones correctamente, hay algunas prácticas recomendadas que debe seguir:

  • Definir una clase base heredando de Exception. Esto permitirá detectar cualquier excepción relacionada con el proyecto (las excepciones más específicas deben heredar de él):

    class MyProjectError(Exception):
        """A base class for MyProject exceptions."""

    Organizar estas clases de excepción en un módulo separado (por ejemplo exceptions.py) es generalmente una buena idea.

  • Para crear una excepción personalizada, subclase la clase de excepción base.

  • Para agregar soporte para argumentos adicionales a una excepción personalizada, defina un __init__()método personalizado con un número variable de argumentos. Llame a la clase base __init__(), pasándole cualquier argumento posicional (recuerde eso BaseException/Exception espere cualquier número de argumentos posicionales ):

    class CustomError(MyProjectError):
        def __init__(self, *args, **kwargs):
            super(CustomError, self).__init__(*args)
            self.foo = kwargs.get('foo')

    Para generar dicha excepción con un argumento adicional, puede usar:

    raise CustomError('Something bad happened', foo='foo')

Este diseño se adhiere al principio de sustitución de Liskov , ya que puede reemplazar una instancia de una clase de excepción base con una instancia de una clase de excepción derivada. Además, le permite crear una instancia de una clase derivada con los mismos parámetros que el padre.

Eugene Yarmash
fuente