Manejo de excepciones y errores en Python
Introducción
Fuente
Antes de entrar en el por qué el manejo de excepciones es esencial y los tipos de excepciones construidasen las excepciones que soporta Python, es necesario entender que hay una sutil diferencia entre un error
y un exception
.
Los errores no se pueden manejar, mientras que las excepciones de Python se pueden manejar en tiempo de ejecución. Un error puede ser un syntax
(parsing), mientras que puede haber muchos tipos de excepciones que podrían ocurrir durante la ejecución y no son incondicionalmente inoperables. Un Error
podría indicar problemas críticos que una aplicación razonable no debería intentar atrapar, mientras que un Exception
podría indicar condiciones que una aplicación debería intentar atrapar. Los errores son una forma de excepción no comprobada y son irrecuperables como un OutOfMemoryError
, que un programador no debería intentar manejar.
El manejo de excepciones hace que tu código sea más robusto y ayuda a prevenir posibles fallos que harían que tu programa se detuviera de forma incontrolada. Imagina que has escrito un código que se despliega en producción y aun así, termina debido a una excepción, tu cliente no lo apreciaría, por lo que es mejor manejar la excepción particular de antemano y evitar el caos.
Los errores pueden ser de varios tipos:
- Error de sintaxis
- Error de falta de memoria
- Error de recursión
- Excepciones
Veámoslas una a una.
Error de sintaxis
Los errores de sintaxis, a menudo llamados errores de parsing, se producen predominantemente cuando el parser detecta un problema sintáctico en tu código.
Tomemos un ejemplo para entenderlo.
a = 8b = 10c = a b
File "<ipython-input-8-3b3ffcedf995>", line 3 c = a b ^SyntaxError: invalid syntax
La flecha de arriba indica cuando el parser se encontró con un error mientras ejecutaba el código. El token que precede a la flecha provoca el fallo. Para rectificar estos errores fundamentales, Python hará la mayor parte de su trabajo ya que imprimirá por ti el nombre del archivo y el número de línea en el que se produjo el error.
Error de falta de memoria
Los errores de memoria dependen en su mayoría de la memoria RAM de tu sistema y están relacionados con Heap
. Si tiene objetos grandes (o) referenciados en la memoria, entonces verá OutofMemoryError (Fuente). Puede ser causado debido a varias razones:
- Usar una arquitectura Python de 32 bits (la asignación máxima de memoria dada es muy baja, entre 2GB – 4GB).
- Cargar un archivo de datos muy grande
- Ejecutar un modelo de Machine Learning/Deep Learning y muchos más.
Puedes manejar el error memory
con la ayuda del manejo de excepciones, una excepción de emergencia para cuando el intérprete se queda completamente sin memoria y debe detener inmediatamente la ejecución actual. En estos raros casos, Python levanta un OutofMemoryError
, permitiendo que el script pueda de alguna manera catch
a sí mismo y break
salir del error de memoria y recuperarse.
Sin embargo, dado que Python adopta la arquitectura de gestión de memoria del lenguaje C (función malloc()), no es seguro que todos los procesos del script se recuperen – en algunos casos, un MemoryError resultará en un crash irrecuperable. Por lo tanto, ni es una buena práctica utilizar el manejo de excepciones para tal error, ni es aconsejable.
Error de recursión
Está relacionado con stack
y ocurre cuando se llaman funciones. Como su nombre indica, recursion
error transpira cuando se ejecutan demasiados métodos, uno dentro de otro (uno con una recursividad infinita), que está limitada por el tamaño de la pila.
Todas tus variables locales y los datos asociados a la llamada de métodos se colocarán en la pila. Para cada llamada a un método, se creará un marco de pila, y tanto los datos locales como los de la llamada al método se colocarán dentro de ese marco de pila. Una vez que la ejecución del método se ha completado, el marco de la pila será eliminado.
Para reproducir este error, vamos a definir una función recursion
que será recursiva, lo que significa que seguirá llamándose a sí misma como una llamada de método de bucle infinito, verá StackOverflow o un Error de Recursión porque el marco de la pila se poblará con los datos del método para cada llamada, pero no será liberado.
def recursion(): return recursion()
recursion()
---------------------------------------------------------------------------RecursionError Traceback (most recent call last)<ipython-input-3-c6e0f7eb0cde> in <module>----> 1 recursion()<ipython-input-2-5395140f7f05> in recursion() 1 def recursion():----> 2 return recursion()... last 1 frames repeated, from the frame below ...<ipython-input-2-5395140f7f05> in recursion() 1 def recursion():----> 2 return recursion()RecursionError: maximum recursion depth exceeded
Error de indentación
El error de indentación es similar en espíritu al error de sintaxis y cae bajo él. Sin embargo, es específico de los únicos problemas relacionados con la indentación en el script.
Así que tomemos un ejemplo rápido para entender un error de indentación.
for i in range(10):print('Hello world')
File "<ipython-input-6-628f419d2da8>", line 2 print('Hello world') ^IndentationError: expected an indented block
Excepciones
Incluso si la sintaxis de una declaración o expresión es correcta, todavía puede causar un error cuando se ejecuta. Las excepciones de Python son errores que se detectan durante la ejecución y no son incondicionalmente fatales: pronto aprenderás en el tutorial cómo manejarlas en los programas de Python. Se crea un objeto de excepción cuando un script de Python lanza una excepción. Si el script no maneja explícitamente la excepción, el programa se verá obligado a terminar abruptamente.
Los programas normalmente no manejan las excepciones, y dan lugar a mensajes de error como los que se muestran aquí:
Error de tipo
a = 2b = 'DataCamp'a + b
---------------------------------------------------------------------------TypeError Traceback (most recent call last)<ipython-input-7-86a706a0ffdf> in <module> 1 a = 2 2 b = 'DataCamp'----> 3 a + bTypeError: unsupported operand type(s) for +: 'int' and 'str'
Error de división cero
100 / 0
---------------------------------------------------------------------------ZeroDivisionError Traceback (most recent call last)<ipython-input-43-e9e866a10e2a> in <module>----> 1 100 / 0ZeroDivisionError: division by zero
Hay varios tipos de excepciones en Python, y el tipo se imprime como parte del mensaje: los tipos en los dos ejemplos anteriores son ZeroDivisionError y TypeError. Ambas cadenas de error se imprimen como el tipo de excepción es el nombre de la excepción incorporada de Python.
La parte restante de la línea de error proporciona los detalles de lo que causó el error basado en el tipo de excepción.
Veamos ahora las excepciones incorporadas de Python.
Excepciones incorporadas
Antes de empezar a aprender las excepciones incorporadas, vamos a repasar rápidamente los cuatro componentes principales del manejo de excepciones, como se muestra en esta figura.
- Try: Ejecutará el bloque de código en el que esperas que se produzca un error.
- Except: Aquí definirás el tipo de excepción que esperas en el bloque try (incorporado o personalizado).
- Else: Si no hay ninguna excepción, entonces se ejecutará este bloque de código (considéralo como un remedio o una opción alternativa si esperas que una parte de tu script produzca una excepción).
- Por último: Independientemente de si hay una excepción o no, este bloque de código siempre se ejecutará.
En la siguiente sección del tutorial, aprenderás sobre el tipo común de excepciones y también aprenderás a manejarlas con la ayuda del manejo de excepciones.
Error de Interrupción del Teclado
La excepción KeyboardInterrupt se produce cuando se intenta detener un programa en ejecución pulsando ctrl+c
o ctrl+z
en una línea de comandos o interrumpiendo el kernel en Jupyter Notebook. A veces puede que no tengas la intención de interrumpir un programa, pero por error, sucede, en cuyo caso usar el manejo de excepciones para evitar estos problemas puede ser útil.
En el siguiente ejemplo, si ejecutas la celda e interrumpes el kernel, el programa lanzará una excepción KeyboardInterrupt.inp = input()Manejemos ahora la KeyboardInterrupt
excepción.
try: inp = input() print ('Press Ctrl+C or Interrupt the Kernel:')except KeyboardInterrupt: print ('Caught KeyboardInterrupt')else: print ('No exception occurred')
Caught KeyboardInterrupt
Error estándar
Aprendamos algunos de los errores estándar que podrían ocurrir habitualmente mientras programamos.
Error Aritmético
- Error de División Cero
- Error de Sobreflujo
- Error de Punto Flotante
Todas las excepciones anteriores caen bajo la clase base Arithmetic
y se plantean por errores en operaciones aritméticas, como se comenta aquí.
División cero
Cuando el divisor (segundo argumento de la división) o el denominador es cero, entonces la resultante plantea un error de división cero.
try: a = 100 / 0 print (a)except ZeroDivisionError: print ("Zero Division Exception Raised." )else: print ("Success, no error!")
Zero Division Exception Raised.
Error de desbordamiento
El Error de desbordamiento se plantea cuando el resultado de una operación aritmética está fuera de rango. OverflowError se levanta para los enteros que están fuera de un rango requerido.
try: import math print(math.exp(1000))except OverflowError: print ("OverFlow Exception Raised.")else: print ("Success, no error!")
OverFlow Exception Raised.
Error de aserción
Cuando una sentencia assert falla, se levanta un Error de aserción.
Tomemos un ejemplo para entender el error de aserción. Digamos que tienes dos variables a
y b
, que necesitas comparar. Para comprobar si a
y b
son iguales o no, aplicas una palabra clave assert
antes, que lanzará una excepción Assertion
cuando la expresión devuelva false.
try: a = 100 b = "DataCamp" assert a == bexcept AssertionError: print ("Assertion Exception Raised.")else: print ("Success, no error!")
Assertion Exception Raised.
Error de atributo
Cuando se hace referencia a un atributo inexistente, y cuando esa referencia o asignación de atributo falla, se lanza un error de atributo.
En el siguiente ejemplo, se puede observar que el objeto de clase Attributes
no tiene ningún atributo con el nombre attribute
.
class Attributes(object): a = 2 print (a)try: object = Attributes() print (object.attribute)except AttributeError: print ("Attribute Exception Raised.")
2Attribute Exception Raised.
Error de importación
El error de importación se produce cuando se intenta importar un módulo que no existe (no se puede cargar) en su ruta estándar o incluso cuando se comete una errata en el nombre del módulo.
import nibabel
---------------------------------------------------------------------------ModuleNotFoundError Traceback (most recent call last)<ipython-input-6-9e567e3ae964> in <module>----> 1 import nibabelModuleNotFoundError: No module named 'nibabel'
Lookup Error
Lookup Error actúa como clase base para las excepciones que se producen cuando una clave o índice utilizado en un mapeo o secuencia de una lista/diccionario no es válido o no existe.
Los dos tipos de excepciones que se plantean son:
- IndexError
- KeyError
Key Error
Si una clave a la que se intenta acceder no se encuentra en el diccionario, se plantea una key
excepción de error.
try: a = {1:'a', 2:'b', 3:'c'} print (a) except LookupError: print ("Key Error Exception Raised.")else: print ("Success, no error!")
Key Error Exception Raised.
Error de índice
Cuando se intenta acceder a un índice (secuencia) de una lista que no existe en esa lista o está fuera del rango de esa lista, se lanza un error de índice.
try: a = print (a) except LookupError: print ("Index Error Exception Raised, list index out of range")else: print ("Success, no error!")
Index Error Exception Raised, list index out of range
Error de Memoria
Como se ha comentado anteriormente, el Error de Memoria se levanta cuando una operación no obtiene suficiente memoria para seguir procesando.
Error de nombre
El error de nombre se produce cuando no se encuentra un nombre local o global.
En el siguiente ejemplo, la variable ans
no está definida. Por lo tanto, obtendrá un name error
.
try: print (ans)except NameError: print ("NameError: name 'ans' is not defined")else: print ("Success, no error!")
NameError: name 'ans' is not defined
Error de tiempo de ejecución
Error no implementado
Esta sección del tutorial se deriva de esta Fuente. El Error de Tiempo de Ejecución actúa como una clase base para el Error No Implementado. Los métodos abstractos de las clases definidas por el usuario deben lanzar esta excepción cuando las clases derivadas sobrescriben el método.
class BaseClass(object): """Defines the interface""" def __init__(self): super(BaseClass, self).__init__() def do_something(self): """The interface, not implemented""" raise NotImplementedError(self.__class__.__name__ + '.do_something')class SubClass(BaseClass): """Implementes the interface""" def do_something(self): """really does something""" print (self.__class__.__name__ + ' doing something!')SubClass().do_something()BaseClass().do_something()
SubClass doing something!---------------------------------------------------------------------------NotImplementedError Traceback (most recent call last)<ipython-input-1-57792b6bc7e4> in <module> 14 15 SubClass().do_something()---> 16 BaseClass().do_something()<ipython-input-1-57792b6bc7e4> in do_something(self) 5 def do_something(self): 6 """The interface, not implemented"""----> 7 raise NotImplementedError(self.__class__.__name__ + '.do_something') 8 9 class SubClass(BaseClass):NotImplementedError: BaseClass.do_something
Error de Tipo
La Excepción de Error de Tipo se lanza cuando se combinan dos tipos de operandos u objetos diferentes o no relacionados.
En el siguiente ejemplo, se suman un entero y una cadena, lo que provoca un type
error.
try: a = 5 b = "DataCamp" c = a + bexcept TypeError: print ('TypeError Exception Raised')else: print ('Success, no error!')
TypeError Exception Raised
Error de valor
El error de valor se produce cuando la operación incorporada o una función recibe un argumento que tiene un type
correcto pero inválido value
.
En el siguiente ejemplo, la operación incorporada float
recibe un argumento, que es una secuencia de caracteres (valor), que no es válido para un tipo float.
try: print (float('DataCamp'))except ValueError: print ('ValueError: could not convert string to float: \'DataCamp\'')else: print ('Success, no error!')
ValueError: could not convert string to float: 'DataCamp'
Excepciones personalizadas de Python
Esta sección del tutorial se deriva de esta Fuente.
Como se estudió en la sección anterior del tutorial, Python tiene muchas excepciones incorporadas que puedes utilizar en tu programa. Aun así, a veces, puede necesitar crear excepciones personalizadas con mensajes personalizados para servir a su propósito.
Puede conseguirlo creando una nueva clase, que derivará de la clase Exception predefinida en Python.
class UnAcceptedValueError(Exception): def __init__(self, data): self.data = data def __str__(self): return repr(self.data)Total_Marks = int(input("Enter Total Marks Scored: "))try: Num_of_Sections = int(input("Enter Num of Sections: ")) if(Num_of_Sections < 1): raise UnAcceptedValueError("Number of Sections can't be less than 1")except UnAcceptedValueError as e: print ("Received error:", e.data)
Enter Total Marks Scored: 10Enter Num of Sections: 0Received error: Number of Sections can't be less than 1
En el ejemplo anterior, como ha observado que si introduce algo menor que 1, se lanzará y manejará una excepción personalizada. Muchos módulos estándar definen sus excepciones para informar de los errores que puedan producirse en las funciones que definen.
Deméritos del manejo de excepciones de Python
Hacer uso del manejo de excepciones de Python también tiene un efecto secundario. Al igual que, los programas que hacen uso de bloques try-except para manejar excepciones se ejecutarán ligeramente más lento, y el tamaño de su código aumentará.
A continuación se muestra un ejemplo donde el módulo timeit
de Python está siendo utilizado para comprobar el tiempo de ejecución de 2 declaraciones diferentes. En stmt1
, se utiliza try-except para manejar ZeroDivisionError
, mientras que en stmt2
, se utiliza la sentencia if
como condición de comprobación normal. A continuación, se ejecutan estas sentencias 10000 veces con la variable a=0. El punto a destacar aquí es que el tiempo de ejecución de ambas sentencias es diferente. Verás que stmt1
, que maneja la excepción, tarda un poco más que stmt2
, que sólo comprueba el valor y no hace nada si no se cumple la condición.
Por lo tanto, deberías limitar el uso del manejo de excepciones de Python y utilizarlo sólo para casos raros. Por ejemplo, cuando no esté seguro de si la entrada será un entero o un flotador para los cálculos aritméticos o no esté seguro de la existencia de un archivo al intentar abrirlo.
import timeitsetup="a=0"stmt1 = '''\try: b=10/aexcept ZeroDivisionError: pass'''
stmt2 = '''\if a!=0: b=10/a'''
print("time=",timeit.timeit(stmt1,setup,number=10000))print("time=",timeit.timeit(stmt2,setup,number=10000))
time= 0.003897680000136461time= 0.0002797570000439009
Felicidades por terminar este tutorial.
Como has aprendido, el manejo excepcional ayuda a romper el flujo de control típico de tu programa proporcionando un mecanismo para desacoplar el manejo de errores de Python y hace que tu código sea más robusto.
El manejo excepcional de Python es uno de los factores primordiales para hacer que tu código esté listo para la producción y sea a prueba de futuro, aparte de añadir pruebas unitarias y programación orientada a objetos.
Es una técnica poderosa y es un concepto de apenas cuatro bloques. El bloque try
busca las excepciones lanzadas por el código, mientras que el bloque except
maneja esas excepciones (incorporadas y personalizadas).
Un buen ejercicio para todos ustedes sería utilizar los cuatro componentes del manejo excepcional y tratar de hacer su código más robusto.