¿Una forma adecuada de declarar excepciones personalizadas en Python moderno?

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

Por “Python moderno” me refiero a algo que se ejecutará en Python 2.5 pero que será “correcto” para Python 2.6 y Python 3. * forma de hacer las cosas. 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 a la excepción.

Me sorprendió 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 la BaseException tenga un significado especial para los atributos llamados message . Supongo que de PEP-352 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 Exception tiene algunos argumentos de parámetros mágicos, 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 intentaban acabar con Args en Python 3.

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

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

 class MyException(Exception): pass 

Editar: para anular algo (o pasar argumentos adicionales), haz 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 a la segunda param, y llegar más tarde con e.errors


Actualización de Python 3: en Python 3+, puedes 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 

Con las excepciones de Python modernas, no necesita abusar de .message , o anular .__str__() o .__repr__() o cualquiera de ellas. Si todo lo que desea es un mensaje informativo cuando se produce la excepción, haga lo siguiente:

 class MyException(Exception): pass raise MyException("My hovercraft is full of eels") 

Eso dará una respuesta que terminará con MyException: My hovercraft is full of eels .

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

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

Sin embargo, obtener esos detalles en un bloque de except es un poco más complicado. Los detalles se almacenan en el atributo args , que es una lista. Necesitarías 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 los índices de tuplas, pero esto es muy desalentador (e incluso fue pensado para su desaprobación hace un tiempo). Si necesita más que una sola pieza de información y el método anterior no es suficiente para usted, entonces debe crear una subclase de Exception como 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 

“¿Una forma adecuada 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 pass da una cadena de documentos:

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

Subclases de Subclases de Excepciones

De los docs

Exception

Todas las excepciones integradas que no se 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 una excepción más específica, haga una subclase de esa excepción en lugar de la Exception genérica (y el resultado será que usted todavía deriva de la Exception como lo recomiendan los documentos). Además, al menos puede proporcionar una cadena de documentos (y no ser forzado a usar la palabra clave pass ):

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

Establezca los atributos que cree usted mismo con un __init__ personalizado. Evite pasar un dictado como un argumento posicional, los futuros usuarios de su código se lo agradecerán. Si usa el atributo de mensaje en desuso, al asignarlo usted mismo evitará una 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 tu propio __str__ o __repr__ . Las integradas son muy agradables, y su herencia cooperativa asegura que usted las use.

Crítica de la respuesta superior.

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

 class MyException(Exception): pass 

Nuevamente, el problema con lo anterior es que para poder capturarlo, tendrá que nombrarlo específicamente (importándolo si se creó en otro lugar) o capturar 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). Una crítica similar a la de abajo, pero adicionalmente esa no es la forma de inicializar a través de super , y obtendrá una DeprecationWarning si accede al atributo del mensaje:

Editar: para anular algo (o pasar argumentos adicionales), haz 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 a la segunda param, y llegar 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 no pueden apreciar.

Para ser directo, viola la sustituibilidad de Liskov .

Voy a demostrar ambos errores:

 >>> ValidationError('foo', 'bar', 'baz').message Traceback (most recent call last): File "", line 1, in  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' 

vea cómo funcionan las excepciones de forma predeterminada si se usan uno o 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 funciona 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 te gusta esa representación predeterminada similar a una tupla, simplemente agrega el método __str__ a la clase ExceptionTemplate , como:

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

y tendrás

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

Debe anular los métodos __repr__ o __unicode__ lugar de usar el mensaje, los argumentos que proporcione cuando construya la excepción estarán en el atributo args del objeto de excepción.

No, el “mensaje” no está prohibido. Es sólo en desuso. Tu aplicación funcionará bien con el uso de mensajes. Pero es posible que desee deshacerse del error de desaprobación, por supuesto.

Cuando creas clases de Excepción personalizadas para tu aplicación, muchas de ellas no son subclases solo de Excepción, 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, por lo general es una buena idea tener una clase base personalizada común para todos ellos, para que los usuarios de sus módulos puedan hacerlo.

 try: ... except NelsonsExceptions: ... 

Y en ese caso, puedes hacer los __init__ and __str__ necesitan, para que no tengas que repetirlos para cada excepción. Pero simplemente llamando a la variable del mensaje para algo más que el mensaje hace el truco.

En cualquier caso, solo necesita __init__ or __str__ si hace algo diferente de lo que hace Exception. Y porque si la depreciación, a continuación, necesita ambos, o recibe un error. Eso no es un montón de código extra que necesita por clase. 😉

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 

Por favor, no olvide documentar, ¡por qué es necesaria una excepción personalizada!

Si lo necesita, esta es la manera de obtener 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(self.payload)) # Detail: Forgot upgrading from Python 1 

payload=None es importante para que pueda ser decapado. Antes de volcarlo, debe llamar al error.__reduce()__ . La carga funcionará como se espera.

Tal vez debería investigar para encontrar una solución usando la statement de return pythons si necesita transferir muchos datos a una estructura externa. Esto me parece más claro / más pythonico. Las excepciones avanzadas se usan mucho en Java, lo que a veces puede ser molesto, al usar un marco y tener que detectar todos los errores posibles.

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