El código puede fallar por muchas razones. Existen diversas técnicas de señalización de fallo y una de las más populares son las excepciones.
Tradicionalmente, una excepción es una situación que desencadena la interrupción del flujo del programa y propaga el control por la pila de funciones llamantes hasta alcanzar el programa principal dónde hace que este se detenga por completo y termine en estado de error.
-
En Python, podemos desencadenar una excepción con la sentencia
raise
:def a(): b() def b(): c() def c(): d() def d(): raise Exception() a()
-
La sentencia
raise
toma un parámetro también llamado excepción, que es un objeto de tipoException
o derivado y que suele contener información para el desarrollador sobre la naturaleza de la excepción:def a(): b() def b(): c() def c(): d() def d(): raise Exception('Intended error') a()
Si, como en el ejemplo anterior, no estamos pasando argumentos a la excepción, podemos devolver la clase del error y Python creará un objeto excepción llamando a la clase sin parámetros:
raise Exception # equivalent to raise Exception()
Python incluye una colección de tipos de errores definida por el lenguaje en sí y por la biblioteca estándar:
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- GeneratorExit
+-- Exception
+-- StopIteration
+-- StopAsyncIteration
+-- ArithmeticError
| +-- FloatingPointError
| +-- OverflowError
| +-- ZeroDivisionError
+-- AssertionError
+-- AttributeError
+-- BufferError
+-- EOFError
+-- ImportError
| +-- ModuleNotFoundError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- MemoryError
+-- NameError
| +-- UnboundLocalError
+-- OSError
| +-- BlockingIOError
| +-- ChildProcessError
| +-- ConnectionError
| | +-- BrokenPipeError
| | +-- ConnectionAbortedError
| | +-- ConnectionRefusedError
| | +-- ConnectionResetError
| +-- FileExistsError
| +-- FileNotFoundError
| +-- InterruptedError
| +-- IsADirectoryError
| +-- NotADirectoryError
| +-- PermissionError
| +-- ProcessLookupError
| +-- TimeoutError
+-- ReferenceError
+-- RuntimeError
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- IndentationError
| +-- TabError
+-- SystemError
+-- TypeError
+-- ValueError
| +-- UnicodeError
| +-- UnicodeDecodeError
| +-- UnicodeEncodeError
| +-- UnicodeTranslateError
+-- Warning
+-- DeprecationWarning
+-- PendingDeprecationWarning
+-- RuntimeWarning
+-- SyntaxWarning
+-- UserWarning
+-- FutureWarning
+-- ImportWarning
+-- UnicodeWarning
+-- BytesWarning
+-- ResourceWarning
Python usa las excepciones para señalizar situaciones extraordinarias, muchas
de ellas, errores, aunque no siempre. Los errores están bajo el tipo
Exception
y estos son algunos de los más comunes:
-
KeyboardInterrupt
: se produce cuando el programa recibe la señal de interrupción, normalmentectrl+C
octr+supr
.while True: pass
Ahora presiona
ctrl+C
para forzar la interrupción. -
AssertionError
: no se cumple la condición delassert
. El mensaje es el segundo parámetro.assert 3 == 5, '3 is not equal to 5'
-
AttributeError
: no se encuentra un atributo en un objeto. El mensaje incluye el atributo y la clase del objeto.o = object() o.answer
-
ImportError
: no se puede importar el nombre indicado. El mensaje incluye el nombre de lo que se quería importar y de dónde se quería importar.from datetime import Date
-
ModuleNotFoundError
: no se encuentra el módulo especificado. El mensaje incluye el nombre del módulo que se trataba de importar.import no_existe
-
IndexError
: el índice se encuentra fuera del rango. No se da información más específica.l = [1,2,3] l[5]
-
KeyError
: no se encuentra la clave en el diccionario. El mensaje es la clave que no se encuentra:d = {'a':1, 'b':2} d['c']
-
FileNotFoundError
: no existen en fichero o el directorio. El mensaje incluye la ruta a la que se intenta acceder.f = open('no/existe', 'r')
-
TimeoutError
: se produce cuando una operación bloqueante excede un tiempo de espera. El mensaje no lleva más información.import http c = http.client.HTTPConnection('example.com', timeout=0.1) c.request('GET', '/noexiste')
-
RecursionError
: se alcanza el máximo de recursión. No se da ninguna información extra.def f(): f() f()
-
SyntaxError
: se produce un error sintáctico. Los detalles se encuentran en el mensaje.for i in range(10) print(i)
-
TypeError
: se produce al tratar de realizar una operación en un tipo no compatible. El mensaje suele incluir los detalles.1 + '2'
-
ValueError
: se produce cuando el valor de un tipo es inválido. El mensaje suele incluir información acerca de por qué el valor es inválido.import datetime new_year = datetime.date(2020, 0, 1)
-
DeprecationWarning
: se produce al tratar de usar una característica obsoleta de un programa. Es normal en bibliotecas de terceros. Los detalles, como por ejemplo, la alternativa preferida se suele encontrar en el mensaje.import warnings def f(): warnings.warn( 'It is preferred if you use `g` instead', DeprecationWarning) g() def g(): ... f()
Es posible definir tipos de error nuevos.
Sabrás cómo cuando hablemos de clases y objetos. Por el momento, cuando tengas que señalizar el error, trata de reutilizar alguna de las existentes. Reutilizar excepciones es importante porque el usuario puede reutilizar el conocimiento que tiene resolviendo excepciones de ese tipo.
Si necesitas una semántica especial, añade un mensaje descriptivo a una
exepción genérica Exception
.
Cuando se produce una excepción, el flujo del programa se interrumpe y el
control trata de abandonar la función actual, transmitiendo el objeto
Exception
, hacia la función llamante.
-
Podemos impedir que una excepción se propage mediante un bloque
try ... except ...
.def safediv(n, divisor): try: return n/divisor except: return float('inf') safediv(10, 0)
Se dice que las operaciones que no lanzan excepciones son seguras. De ahí el nombre.
-
Podemos refinar la captura de la excepción para distinguir entre un tipo de excepción y otro:
def safediv(n, divisor): try: return n/divisor except ZeroDivisionError: return float('inf') except TypeError: return float('nan') safediv(10, 0)
Una cláusula
except
es compatible si no indica la clase (el caso más general) o si contiene la clase de la excepción o una clase padre. La primera cláusula compatible, se ejecuta:try: import no_existe except ModuleNotFoundError: print('ModuleNotFoundError') except ImportError: print('ImportError')
Prueba a invertir las cláusulas. ¿Qué ocurre?
-
También podemos recoger varias excepciones con una misma cláusula:
try: import no_existe except (ModuleNotFoundError, ImportError) as ex: print('ModuleNotFoundError or ImportError?', type(ex))
En realidad, la lista de errores puede ser cualquier tupla:
some_errors = (ModuleNotFoundError, ImportError) try: import no_existe except some_errors as ex: print('ModuleNotFoundError or ImportError?', type(ex))
-
Si queremos recoger la excepción en sí, usaremos las siguiente fórmula:
def log_module_absence(name): ... try: import no_existe except ModuleNotFoundError as ex: log_module_absence(ex.name) raise ex
Equivalentemente, cuando queramos elevar la misma excepción que hemos capturado, podemos hacer:
def log_module_absence(name): ... try: import no_existe except ModuleNotFoundError as ex: log_module_absence(ex.name) raise # equivalent to raise ex
-
Hay situaciones en las que queremos ejecutar código tanto si se produce una excepción como si no. Por ejemplo:
def do_computations(*args): raise Exception def store_computations(file, *args): f = open(file, 'w') for arg in args: result = do_computation(arg) f.write(result) f.close() # very important: close before returning return result
Es importante y parte del contrato de uso de los ficheros, cerrar el fichero cuando hayamos terminado de utilizarlo. Dejarlo abierto lleva a comportamientos indefinidos, a menudo no deseados.
-
Si se produce una excepción, queremos también cerrar el fichero:
def do_computations(*args): raise Exception def store_computations(file, *args): try: f = open(file, 'w') for arg in args: result = do_computation(arg) f.write(result) f.close() return result except: f.close() # very important: close before returning raise
-
En lugar de utilizar este patrón, es mejor usar la cláusula
finally
:def do_computations(*args): raise Exception def store_computations(file, *args): try: f = open(file, 'w') for arg in args: result = do_computation(arg) f.write(result) finally: f.close() return result
A veces te encontrarás escribiendo código con esta pinta:
# set-up code
...
...
# main code
...
...
# clean-up code
...
...
Por ejemplo, en el caso anterior:
def store_computations(file, *args):
# set-up
try:
f = open(file, 'w')
# main code
for arg in args:
result = do_computation(arg)
f.write(result)
# clean-up
finally:
f.close()
return result
Para abstraer y sintetizar este patrón utilizamos contextos. Los contextos son una de las características más prominentes de Python 3 y le dedicaremos más tiempo cuando estudiemos protocolos.
Por el momento, baste decir que los contextos son objetos que incluyen código de inicialización (set-up) y limpieza (clean-up) predefinidos. Los contextos pueden, también, capturar y manejar las excepciones que ocurran durante la ejecución del código principal (main code).
-
Un contexto se usa con la sintáxis
with
. El caso de los ficheros es el ejemplo paradigmático: aún mejor que usar una cláusulafinally
es usar un bloquewith
:def store_computations(file, *args): with open(file) as f: result = do_computation(*args) f.write(result) return result
-
Otro contexto de utilidad es la supresión de una excepción. En lugar de hacer:
try: raise ValueError: except: pass
Hacemos, de forma más clara y semántica:
from contextlib import suppress with suppress(Exception): raise ValueError
Observa las diferencias entre este contexto y el contexto del ejemplo anterior. El contexto devuelto por
open
no suprime la excepción, sólo la captura, cierra el fichero, y la relanza. El contexto devuelto porsuppress(Exception)
, captura y suprime cualquier excepción.
-
Si estás interesado en otros contextos, échale un vistazo al módulo contextlib.
-
Los bloques
try ... except ...
pueden tener una cláusulaelse
con código del que no esperes ningún error (código seguro).
La información que acompaña a una excepción es vital para depurar el problema. El mensaje, dónde se produjo y la pila de llamadas son importantes. Python, además, permite indicar que una excepción fue la causa de otra.
-
Considera el siguiente código:
class LibraryError(Exception): ... def do_computations(*args): raise Exception('Invalid computation') def do_something(*args): try: do_computations(*args) except Exception: print('Something bad happened') raise LibraryError('Library cannot do as instructed.') do_something()
Fíjate en que la excepción indica exactamente lo que ha pasado:
During handling of the above exception, another exception occurred
. -
Sin embargo, no es que haya pasado "algo"; lo que queríamos decir es que la excepción de
do_computations
causa la excepción dedo_something
. Esto se puede indicar de manera más explícita con una sentenciaraise ... from ...
:class LibraryError(Exception): ... def do_computations(*args): raise Exception('Invalid computation') def do_something(*args): try: do_computations(*args) except Exception as ex: print('Something bad happened') raise LibraryError('Library cannot do as instructed.') from ex do_something()
Fíjate cómo ahora indica:
The above exception was the direct cause of the following exception
, significando que la excepción dedo_computations
es la causa de la excepción dedo_something
. -
Sin embargo, a veces es posible que se quiera ocultar la causa de la excepción. Si este es el caso, se deberá usar:
class LibraryError(Exception): ... def do_computations(*args): raise Exception('Invalid computation') def do_something(*args): try: do_computations(*args) except Exception as ex: print('Something bad happened') raise LibraryError('Library cannot do as instructed.') from None do_something()
-
A estos tres estilos anteriores falta sumarle la mera propagación de una excepción (es decir, no tiene otra causa) que ya has aprendido:
class LibraryError(Exception): ... def do_computations(*args): raise Exception('Invalid computation') def do_something(*args): try: do_computations(*args) except Exception: print('Something bad happened') raise do_something()
Se ha hablado mucho del coste de las excepciones. Es importante atender al nombre de esta herramienta. Una excepción debería ocurrir excepcionalmente. Si una situación puede ocurrir con cierta frecuencia, deja de ser algo excepcional.
Alguna literatura al respecto: