¿Forma correcta de declarar excepciones personalizadas en Python moderno?

1510

¿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 sea impresa por cualquier herramienta que haya detectado la excepción.

Por "Python moderno" me refiero a algo que se ejecutará en Python 2.5 pero que será "correcto" para la forma de hacer las cosas de 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. Deduzco de PEP-352 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. Puaj.

También soy vagamente consciente de que Exceptiontiene algún parámetro mágico args, pero nunca supe 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 sugirieron anular __init__y __str__/ __unicode__/ __repr__. Eso parece mucho tecleo, ¿es necesario?

1
  • 11
    Creo que este es uno de esos casos en los que Python no sigue uno de sus propios aforismos: There should be one-- and preferably only one --obvious way to do it. 29/06/20 a las 12:07
1565
+50

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

class MyException(Exception):
    pass

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().__init__(message)
            
        # Now for your custom code...
        self.errors = errors

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

En Python 2, debes usar esta forma un poco más compleja de super():

super(ValidationError, self).__init__(message)
4
  • 51
    Sin embargo, una excepción definida como esta no sería seleccionable; vea la discusión aquí stackoverflow.com/questions/16244923/…
    jiakai
    1 de agosto de 2017 a las 2:54
  • 128
    @jiakai significa "encurtido". :-)
    Robino
    15 de septiembre de 2017 a las 13:39
  • 3
    Siguiendo la documentación de Python para las excepciones definidas por el usuario, los nombres que se mencionan en la función __init__ son incorrectos. En lugar de (yo, mensaje, error) es (yo, expresión, mensaje). La expresión de atributo es la expresión de entrada en la que ocurrió el error y el mensaje es una explicación del error.
    ddleon
    27 feb.20 a las 15:13
  • 6
    Eso es un malentendido, @ddleon. El ejemplo de los documentos a los que se refiere es para un caso de uso particular. No tiene importancia el nombre de los argumentos del constructor de la subclase (ni su número).
    asthasr
    22/04/20 a las 21:15
580

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 esto:

class MyException(Exception):
    pass

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

Eso le dará un rastreo que termina con 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. Debería 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 varios elementos a la excepción y acceder a ellos a través de índices de tupla, pero esto está muy desaconsejado (e incluso estaba destinado a ser obsoleto hace un tiempo). Si necesita más de una pieza de información y el método anterior no es suficiente para usted, entonces debe crear una subclase 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
5
  • 2
    "pero esto quedará obsoleto en el futuro", ¿todavía está previsto que se desactive? Python 3.7 todavía parece aceptar felizmente Exception(foo, bar, qux). 20/04/18 a las 22:36
  • No ha visto ningún trabajo reciente para eliminarlo desde el último intento fallido debido al dolor de la transición, pero ese uso aún se desaconseja. Actualizaré mi respuesta para reflejar eso. 2 de mayo de 2018 a las 8:20
  • @frnknstn, ¿por qué se desaconseja? Me parece un buen modismo.
    neves
    8 de mayo de 2018 a las 17:48
  • 3
    @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 10 de mayo de 2018 a las 8:09
  • 3
    La sección relevante de PEP352 es "Ideas retractadas " . 17/04/19 a las 16:25
233

"What is the proper way to declare custom exceptions in modern Python?"

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"""

Subclasificación de subclases de excepción

De los documentos

Exception

All built-in, non-system-exiting exceptions are derived from this class. All user-defined exceptions should also be derived from this class.

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á que todavía se derive Exceptioncomo lo recomiendan los documentos). Además, puede al menos proporcionar una cadena de documentación (y no estar obligado a usar la passpalabra clave):

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

Establezca los atributos que crea usted mismo con un archivo personalizado __init__. Evite pasar un dictado como argumento posicional, los futuros usuarios de su código se lo agradecerán. Si usa el atributo de mensaje obsoleto, asignándolo usted mismo evitará 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) 

Realmente no hay necesidad de escribir el tuyo propio __str__o __repr__. Los integrados son muy agradables y su herencia cooperativa asegura que los use.

Crítica de la respuesta principal

Maybe I missed the question, but why not:

class MyException(Exception):
    pass

Nuevamente, el problema con lo anterior es que para detectarlo, tendrá que nombrarlo específicamente (importándolo si se creó en otro lugar) o detectar la Excepción (pero probablemente no esté preparado para manejar todos los tipos 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:

Edit: to override something (or pass extra args), do this:

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

That way you could pass dict of error messages to the second param, and get to it later with e.errors

También requiere que se pasen exactamente dos argumentos (además del self.) Ni más, ni menos. Esa es una restricción interesante que los futuros usuarios tal vez no aprecien.

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'

En comparación con:

>>> MyAppValueError('foo', 'FOO', 'bar').message
'foo'
12
  • 3
    ¡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
    3 de ene. De 2018 a las 18:21
  • 8
    @Kos La crítica sobre la sustituibilidad de Liskov sigue siendo válida. La semántica del primer argumento como un "mensaje" también es discutible, pero no creo que voy a discutir el punto. Le daré un vistazo más a esto cuando tenga más tiempo libre.
    Aaron Hall
    3 de enero de 2018 a las 19:02
  • 2
    @ostergaard No puede 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
    16 de septiembre de 2018 a las 21:17
  • 3
    No veo mucho sentido en seguir el principio de sustitución de Liskov con excepciones personalizadas. Presenta una excepción específica para indicar una condición específica. ¿Por qué alguna vez necesitaría sustituir una instancia de una clase de excepción base con una instancia de una clase de excepción derivada? 28 feb.20 a las 15:34
  • 2
    @AaronHall Nunca he necesitado esto en la práctica. De todos modos, supongo que su ejemplo también viola LSP: si su código usa raise ValueError(msg), no puede reemplazar esto con raise ValidationError(msg)ya que este último requiere un parámetro más. 28 feb.20 a las 18:02
57

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

>>> 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 una 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 predeterminada tipo tupla, simplemente agregue un __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
49

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 es necesario, este es el camino a seguir para las 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 tráelos 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 hacerlo encurtido. Antes de tirarlo, debes llamar error.__reduce__(). La carga funcionará como se esperaba.

Tal vez debería investigar para encontrar una solución utilizando la returndeclaración de pitones si necesita que se transfieran muchos datos a alguna estructura externa. Esto me parece más claro / más pitónico. 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.

3
  • 3
    Como mínimo, 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
    26/03/19 a las 16:46
  • 5
    Pregunta honesta: ¿Por qué es importante que las excepciones se puedan encurtir? ¿Cuáles son los casos de uso para las excepciones de carga y descarga? 6/08/19 a las 15:21
  • 2
    @RoelSchroeven: Tuve que paralelizar el código una vez. Funcionó bien con un solo proceso, pero algunos aspectos de algunas de sus clases no eran serializables (la función lambda se pasaba como objetos). Me tomó algo de tiempo resolverlo y arreglarlo. Lo que significa que alguien más tarde puede terminar necesitando que se serialice su código, no pueda hacerlo y tenga que averiguar por qué ... Mi problema no fueron errores impredecibles, pero puedo ver que causa problemas similares. 6/10/19 a las 2:58
24

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

  • Defina una clase base heredada de Exception. Esto permitirá detectar fácilmente cualquier excepción relacionada con el proyecto:

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

    Organizar las clases de excepción en un módulo separado (p exceptions.py. Ej. ) Es generalmente una buena idea.

  • Para crear una excepción específica, cree una subclase de 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 que BaseException/Exception espero cualquier número de argumentos posicionales ):

    class CustomError(MyProjectError):
        def __init__(self, *args, **kwargs):
            super().__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.

4
  • 3
    Realmente me gusta este diseño ... Siento que es mucho más limpio que los de las otras respuestas. 14/07/20 a las 5:23
  • La adhesión a LSP debería ser obligatoria, por eso prefiero esta respuesta a las demás.
    loutre
    17 jul.20 a las 22:19
  • ¿Cómo probamos si esta excepción se lanza o no se utilizan pruebas unitarias?
    james
    18/12/20 a las 10:33
  • ¿Sería decapable? 29/12/20 a las 16:35
18

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

11

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) Exception.
  • Llame siempre BaseException.__init__con un solo argumento.
  • Al construir una biblioteca, defina una clase base heredada de Exception.
  • Proporcione detalles sobre el error.
  • Herede de los tipos de excepciones incorporados cuando tenga sentido.

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

4
  • 1
    Este es un buen ejemplo de por qué en SO normalmente verifico la respuesta más votada, pero también las más recientes. Útil además, gracias. 5/10/19 a las 18:09
  • 4
    Always call BaseException.__init__ with only one argument.Parece una restricción innecesaria, ya que en realidad acepta cualquier número de argumentos. 29 feb.20 a las 13:12
  • @EugeneYarmash Estoy de acuerdo, ahora no lo entiendo. No lo uso de todos modos. Quizás debería releer el artículo y ampliar mi respuesta. 29/02/20 a las 13:56
  • @EugeneYarmash Leí el artículo nuevamente. Se indica que en el caso de varios argumentos, la implementación de C llama a "return PyObject_Str (self-> args);" Significa que una cadena debería funcionar mejor que varias. ¿Revisaste eso? 3 mar.20 a las 16:51
10

No, "mensaje" no está prohibido. Simplemente está en desuso. Su aplicación funcionará bien con el uso de 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 Exception, sino de otras, como ValueError o similar. Entonces tienes que adaptarte 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, para que los usuarios de sus módulos puedan hacer

try:
    ...
except NelsonsExceptions:
    ...

Y en ese caso, puede hacer lo __init__ and __str__necesario allí, por lo que no tiene que repetirlo para cada excepción. Pero simplemente llamar a la variable de mensaje de otra manera que mensaje hace el truco.

En cualquier caso, solo necesita el __init__ or __str__si hace algo diferente de lo que hace Exception. Y porque si la desaprobación, entonces necesita ambos, o obtendrá un error. Eso no es mucho código adicional que necesita por clase. ;)

2
  • 1
    Es interesante que las excepciones de Django no hereden de una base común. docs.djangoproject.com/en/2.2/_modules/django/core/exceptions ¿Tiene un buen ejemplo cuando se necesita capturar todas las excepciones de una aplicación específica? (tal vez sea útil solo para algunos tipos específicos de aplicaciones). 10/06/19 a las 20:05
  • 1
    Encontré un buen artículo sobre este tema, julien.danjou.info/python-exceptions-guide . Creo que las excepciones deben subclasificarse principalmente basadas en dominios, no basadas en aplicaciones. Cuando su aplicación trata sobre el protocolo HTTP, se deriva de HTTPError. Cuando parte de su aplicación es TCP, obtiene las excepciones de esa parte de TCPError. Pero si su aplicación abarca muchos dominios (archivo, permisos, etc.), la razón para tener una MyBaseException disminuye. ¿O es para protegerse de la 'violación de capa'? 10/06/19 a las 20:44
4

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")
4

Un enfoque realmente simple:

class CustomError(Exception):
    pass

raise CustomError("Hmm, seems like this was custom coded...")

O haga que el error aumente sin imprimir __main__(puede parecer más limpio y ordenado):

class CustomError(Exception):
    __module__ = Exception.__module__

raise CustomError("Improved CustomError!")