12 - Curso de Python - Try Exception

De ChuWiki

Cualquier duda suelo atender en este foro de python

Todo el código de este curso de Python gratuito está en github https://github.com/chuidiang/chuidiang-ejemplos/tree/master/PYTHON/curso-python. En línea comandos python tienes cómo arrancar la consola de comandos de python por si quieres ir probando los ejemplos de manejo de excepciones (try except) en Python.

Anterior: 11 - Curso de Python - Bases de datos -- Índice: Curso de Python -- Siguiente: 13 - Curso de Python - Leer y escribir ficheros en python.

Qué son excepciones en Python[editar]

Excepciones en Python son errores que se producen durante la ejecución de nuestro código por una situación ineseperada. Por ejemplo, si haces una divisíón por cero obtienes una excepción

>>> 1/0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

Este es un ejemplo muy directo. Al dividir por cero obtenemos una excepción ZeroDivisionError. Nadie va a escribir ese código. Pero sí puede pasar si haces un progama que lea los datos de algún sitio o pida datos al usuario. Por ejemplo.

>>> a = input ('Introduce un numero ')
Introduce un numero abc
>>> print ('Tu numero mas 1 es ', int(a)+1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'abc'

El usuario ha sido un poco "malo" y cuando le hemos pedido un número, ha metido tres letras. Nuestro programa pretendía sacar ese número más uno. Pero ha dado una excepción ValueError.

Si ocurre un error y no hacemos nada para tratarlo, el programa termina mostrando el error. Por ello, es interesante capturar y tratar los posibles errores que se produzcan, de forma que nuestro programa pueda seguir funcionando o al menos, no termine de forma brusca. Veamos como hacerlo en los siguientes apartados.

Manejo de las excpeciones[editar]

Python tiene la sentencia try except else finally para tratar este tipo de errores inesperados. El esqueleto completo de esta sentencia es

try:
  sentencias1
except:
  sentencias2
else:
  sentencias3
finally:
  sentencias4

sentencias1 es el bloque de código donde prevemos que puede haber este error inesperado. Es decir, código donde hagamos operaciones matemáticas susceptibles, por ejemplo, de divisiones por cero. Código donde se convierten cadenas de texto a enteros. Códido donde se abre y lee un fichero que puede fallar porque el fichero no exista o no tengamos permisos para leerlo. Y un largo etcétera de posibles trozos de código donde puede haber errores inesperados, pero que sabemos que son posibles.

Si durante la ejecución de sentencias1 se produce un error, dejan de ejecutarse las que queden y se va inmediatamente al bloque except. Ahí podemos intentar arreglar el error, parar la operación y sacar un mensaje de error al usuario, etc. Lo que consideremos en cada caso que pueda ayudarnos a salir airosos de una situación de error inesperado.

Si en sentencias1 no se produce ningún error, entonces se ejeucta el bloque else. Este bloque es opcional, puede no estar. Sería como un conjunto de sentencias adicionales que queremos ejecutar si sentencias1 ha ido bien y, por supuesto, no prevemos que sentencias3 pueda dar algún error. Como buena costumbre de programación, en el bloque try debemos poner solo aquellas sentencias susceptibles de fallo. El resto de código que hagamos que no sea susceptible de fallo, debe ir en el else. Por ejemplo, si queremos hacer una división y sacar el resultado en pantalla, en el try hacemos la división. Una vez hecha sin error, en el bloque else sacamos el resultado por pantalla.

Y tanto si sentencias1 da error como si no, el bloque finally se ejecutará siempre al final. Este bloque es útil para cerrar recursos que hayamos abierto en sentencias1. Por ejemplo, imagina que en sentencias1 abrimos un fichero para escribir datos en él. Escribimos datos y como última sentencia1 cerramos el fichero. Algo así

try:
   abrir fichero
   escribir datos en el fichero
   cerrar el fichero
...

¿Cual es el problema de esto?. Imagina que abrimos el fichero pero salta una excpeción al escribir datos. El cierre del fichero ya no se ejecutaría. Por ello, es buena idea llevárselo al finally, así

try:
   abrir fichero
   escribir datos en el fichero
except:
   ha habido algún error al abrir/escribir en fichero
finally:
   cerrar fichero si está abierto.

Esta es la forma más segura de no dejar recursos abiertos. Quien dice ficheros, dice conexiones a base de datos, conexiones de red y en general, cualquier recuros que se abre para trabajar con él y se cierra al acabar.

Capturar excepciones concretas[editar]

Hemos comentado que except se ejecuta cuando hay algún error y que else se ejecuta si no se produce ningún error. Esto es cierto con lo que hemos puesto hasta ahora, pero hay más posibilidades. Si un bloque try puede producir varios tipos de errores, por ejemplo, dos de los que ya hemos visto ZeroDivisionError y ValueError, podemos poner tantos except como tipos de error distintos queramos capturar. Por ejemplo

try:
   sentencias1
except ZeroDivisionError as error_division:
   sentencias2
excpet ValueError as value_error:
   sentencias3
else:
   sentencias4

La parte as variable es opcional. Solo la necesitamos si queremos tener el error como una variable para sacar algo específico de él en pantalla. Si sentencias1 producde uno de esos dos errores, el código irá al except adecuado. Pero si produce un error distinto, no se capturará, pero tampoco se ejecutará el else. Podemos poner un último except justo antes de else. Este except capturará cualquier excepción no contemplada en los casos anteriores. Por ejemplo

>>> try:
...    print(1/0)
... except ValueError:
...    print('Un ValueError')
... except Exception as error:
...    print('Un error de tipo ',type(error))
... else:
...    print('Sin errores')
...
Un error de tipo  <class 'ZeroDivisionError'>

Hemos hecho una división por cero en el bloque try. Capturamos la excepción ValueException y luego otra general, de tipo Excepction, que metemos en la variable error. En este bloque sacamos por pantalla el tipo de excepción que es. Ponemos un bloque else aunque no se ejecutará, puesto que en el bloque try estamos haciendo una división por cero que producirá una excepción. Vemos que el resultado al ejecutar esto es que se ha producido una excepción de tipo ZeroDivisionError.

Si queremos capturar varias excepciones concretas y con todas ellas hacer lo mismo, se admite esta sintaxis

>>> try:
...    print(1/0)
... except (ValueError, ZeroDivisionError) as error:
...    print('Error de tipo ',type(error))
...
Error de tipo  <class 'ZeroDivisionError'>

Podemos poner entre paréntesis, detrás de except, todas las excepciones que queramos.

Cadena de excepciones[editar]

Cuando capturamos una excepción, podemos tratarla, sacarla por pantalla o hacer lo que considermos. Pero también podemos después de tratarla, relanzarla para que la trate también alguien más. Como ejemplo, metemos el siguiente código en un fichero "division.py".

def divide(dividendo,divisor):
    try:
        cociente = dividendo/divisor
    except ZeroDivisionError as error:
        print ('Division por cero ', str(error))
        raise
    else:
        return cociente

try:
    resultado = divide(1,0)
except Exception as error:
    print ('Algo ha ido mal', str(error))
else:
    print ('1/0 es ', resultado)

Hacemos una función que divide dos números que le pasan. Ponemos el try y toda la parafernalia except y else. Unicamente fíjate que en el except hemos puesto al final un raise, sin parámetros. Esto hace que aunque tratemos la excepción, esta siga su curso como si no la hubieramos tratado. Por lo que el trozo de código que hay a continuación, cuando llame a divide(1,0), recibirá también el error y podrá tratarlo también. Podemos ejecutar esto desde línea de comandos de windows/terminal linux y obtendremos:

C:\> python
Division por cero  division by zero
Algo ha ido mal division by zero

Vemos los dos print de ambos except. Es decir, los dos han podido tratar la excepción.

Pero para poder tener más trazabilidad, podemos poner una sintaxis como raise una_excepcion from otra_excepcion. Me explico. Ampliamos un poco el código

import traceback

class ApplicationError(Exception):
    pass

def divide(dividendo,divisor):
    try:
        cociente = dividendo/divisor
    except ZeroDivisionError as error:
        raise ApplicationError('funcion divide()') from error
    else:
        return cociente

try:
    resultado = divide(1,0)
except Exception as error:
    traceback.print_exc()
else:
    print ('1/0 es ', resultado)

El código es prácticamente igual salvo los siguientes cambios:

  • importamos el módulo traceback. Esto nos permite sacar por pantalla toda la cadena de excepciones que han ido saltando.
  • Hemos creao una excpecion propia de nuestra aplicación: ApplicationError. No te preocupes ahora por la sintaxis, vemos los detalles de cómo crear excepciones propias en el siguiente apartado. Aquí quédate sólo con la idea de que le hemos dicho a Python que hay una excpeción nueva que se llama ApplicationError
  • La función divide no saca error por pantalla en except. Unicamente y aquí uno de los puntos importantes del ejemplo, lanza una ApplictionError "from" el error que se ha producido, es decir, la ZeroDivisionErro. Esto deja registrado internamente que la excepción ApplicationError se ha producido por una ZeroDivisionError.
  • En el código que usa la división, en el bloque except, ponemos una llamada a traceback.print_exc(). Esta llamda, dentro del bloque except saca por pantalla la excepción que se ha producido con toda la cadena de excepciones que lo ha provocado. Veamos la salida de este programa para ver qué queremos decir.
C:\> python division.py
Traceback (most recent call last):
  File "C:\Users\fjabe\Proyectos\chuidiang-ejemplos\PYTHON\curso-python\12-Try-Exception\division.py", line 8, in divide
    cociente = dividendo/divisor
ZeroDivisionError: division by zero

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\Users\fjabe\Proyectos\chuidiang-ejemplos\PYTHON\curso-python\12-Try-Exception\division.py", line 15, in <module>
    resultado = divide(1,0)
  File "C:\Users\fjabe\Proyectos\chuidiang-ejemplos\PYTHON\curso-python\12-Try-Exception\division.py", line 10, in divide
    raise ApplicationError('funcion divide()') from error
ApplicationError: funcion divide()

Bueno, es una salida un poco engorrosa, pero leyendo de arriba a abajo y fijándose en las líneas de interés:

  • Una ZeroDivisionError en la línea 8 del fichero division.py, en cociente = dividendo/divisor. Esta es la causa origen del error.
  • Ese error ha provocado después una ApplicationError en la línea 15 de division.py, en resultado = divide(1,0).
  • Y este error ha sido lanzado desde la línea 10 de divide.py, en raise ApplicationError('funcion divide()') from error.

Es decir, con este log en pantalla y usando en código cosas como raise una_excepcion from otra_excepcion, podemos seguir todo el rastro del error desde su origen hasta el sitio que ha sido sacado por pantalla. Esta es muy útil para depurar cuando hay errores que no tenemos totalmente controlados o inesperados.

Excepciones propias de la aplicación[editar]

En nuestro programa tenemos la posiblidad de crear excepciones propias de nuestra aplicación para lanzarlas cuando necesitemos. Imagina que alguien introduce un usuario y password y cuando tu aplicación lo verifica ve que no es correcto. Podemos crear una excepción propia para esta circunstancia y luego lanzarla cuando corresponda. Para crearla, basta hacer una clase que herede de Exception

>>> class WrongUserPassword(Exception):
...    pass
...


>>> try:
...   # Verificar usuario password.
...   # Si es incorrecto, lanzamos la excepcion.
...   raise WrongUserPassword()
... except WrongUserPassword:
...   print('Usuario/Password no valido')
...
Usuario/Password no valido

Hemos creado la clase WrongUserPassord que hereda de Exception. No queremos liarnos rellenando código de esa excpeción ahora, así que la dejamos vacía poniendo la sentencia pass que no hace nada, pero sirve para ponerla donde obligatoriamente tengamos que meter algo.

Luego, en un try, no hemos verificado usuario/password para no complicar el ejemplo, solo hemos puesto un comentario donde iría esa verificación. Si no el usuario y password no es válido, lanzamos nuestra excepción con raise WrongUserPassword().

Podríamos añadir información adicional a la hora de lanzar la excpeción. De hecho, la clase Excepcion admite en el constructor N parámetros que se guarda internamente. Fijate en esto

>>> error = Exception('hola','soy',1,'excepcion')
>>> str(error)
"('hola', 'soy', 1, 'excepcion')"

Hemos creado una Exception pasando cuatro parámetros cualesquiera. Si llamamos a str() pasando la excepción, saca los parámatros por pantalla. Asi que si heredamos de Excepcion, tenemos esto de regalo en nuestra excepción. En nuestro ejemplo de WrongUserPassword, sin tocar la clase ya creada, podemos hacer

>>> import datetime
>>> error = WrongUserPassword('Juan', datetime.datetime.now())
>>> str(error)
"('Juan', datetime.datetime(2022, 8, 6, 12, 34, 22, 22338))"

Lo que nos permite guardar parámetros de interés en cualquier excpeción nuestra. En este ejemplo, hemos puesto el nombre de usuario y la fecha/hora del intento fallido. Por supuesto, podemos al definir nuestra excepción propia definir constructores o lo que necesitemos, aunque suele ser buena costumbre de programación dejar las clases de excepción lo más sencillas posibles.

Arbol de herencia de excepciones[editar]

Phython ya tiene un arbol de herencias de excepciones. La primera es BaseException y de ahí van heredando las demás. Suele haber una excpecion padre por cada grupo relacionado de excepciones y luego ya las excepciones concretas de ese grupo. La siguiente tabla muestra el árbol de herencia de las excepciones en python

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
           +-- EncodingWarning
           +-- ResourceWarning

Python aconseja que las excepciones propias de nuestra aplicación las heredemos a partir de Excepcion, como hemos hecho. Solo como ejemplo de jerarquía, fíjate por ejemplo que hay una excepción ArithmeticError y debajo de ella cuelgan algunas concretas, como la ZeroDivisionError que hemos estado usando hasta ahora.

Buenas constumbres a la hora de crear excepciones propias en python[editar]

Así que si quieres crear tus propias excepciones, suelen ser buenas costumbres:

  • Heredando de Excepcion, pensar tu jerarquía propia de excepciones, o bien extender las ya existentes. Si haces un programa de contabilidad, quizás puedas hacer tu excepción padre ContabilidadError que herede de Exception y luego ya excepciones concretas como podrían ser SaldoInsuficienteError, TransferenciaError, etc, etc.
  • Suele terminarse el nombre de la excepción en "Error", así es fácilmente reconocible que la clase es una excepción. O Como ves en el árbol de Python, "Warning" si no son excepciones graves.
  • Tus excpeciones convienen que estén en un módulo (fichero .py) propio de excepciones, por ejemplo, errores_contabilidad.py o algo así. El motivo es que las excepciones seguramente se usen a lo largo de todo tu programa. Si mezclas tus excepciones en módulos de tu programa que tienen otras clases y funciones que hacen otras cosas, tendrás que importar todo aunque sólo quieras usar la excepción. Aparte, que tendrás que recordar en qué módulo estaba definida.


Anterior: 11 - Curso de Python - Bases de datos -- Índice: Curso de Python -- Siguiente: 13 - Curso de Python - Leer y escribir ficheros en python.