Manejo de Excepciones en Python

Mariano Rivera

Tabla of Contenidos

¿Que es una excepción?

Es un evento, probablemente catastrófico, durante la ejecución de un programa debido situaciones ajenas al propósito del algoritmo.

Motivación: Cálculo del Factorial

Asuma que nos piden implementar un programa para calcular el factorial usado la fórmula
n!=n(n1)! n! = n(n-1)!
con 0!=def10! \overset{def}{=}1.

def factorial(n):
    if n==0:
        return 1
    return n*factorial(n-1)

F = factorial(5)
print(F)
120

Ahora intentemos

F = factorial(-1)

Esto genera un error de ejecución, porque nuestro algoritmo no esta diseñado para calcular factoriales de números negativos. La razón es que los factoriales de números negativos no estan definidos.

¿Que debemos hacer? ¿Como modificar nuestra función factorial para este caso?

Lo que no debemos hacer es regresar un valor y dejar a quién llama a nuestra función la responsabilidad de interpretarlo: Debemos generar una excepción.

Estatutos de Pyhton para manejo de excepciones

El manejo de exepciones en Python usa los estatutos (statements):

Aunque generalmente, dado que else y finally son opcionales, es común encontrar solo la combinación:

Significado:

Factorial con exepción por enteros negativos

Ahora si, rescribimos la función factorial con el manejo de excepción de números negativos

def factorial(n):
    try: 
        if n <0:
            raise Exception('negative')
    except Exception as inst:
        print('Error: factorial of {} numbers is not defined!'.format(inst))
    else:
        if n==0:
            return 1
        return n*factorial(n-1)

print(factorial(5))
print(factorial(0))
print(factorial(-1))
120
1
Error: factorial of negative numbers is not defined!
None

Manejamos la excepción para enteros negativos. Pero, ¿que pasa con los siguientes casos?

print(factorial(3.1))
print(factorial('hola'))
print(factorial([3,4]))
print(factorial(3+7j))

Se deja el ejercicio de escribir el manejo de la exepción para parámetros no enteros en general.

División por cero

Ejemplo tomado de la documentación de Python

def divide(x, y):
    try:
        result = x / y
    except ZeroDivisionError:
        print("division by zero!")
    else:
        print("result is", result)
    finally:
        print("executing finally clause")

Abrir un archivo

ver el código a continuación

file = 'file.txt'
try:
    f = open(file, 'r')    
except IOError as error:
    print("{} could not be opened: {}".format(file, error.strerror))
else:
    A = print('we can read {1} \nwith object-file: {0}'.format(f, file))
finally:
    print("Done processing %s" % file)
we can read file.txt 
with object-file: <_io.TextIOWrapper name='file.txt' mode='r' encoding='UTF-8'>
Done processing file.txt

Creación de un nuevo directorio, asegurando que no exista con antelación

Propósito: crear el directorio con el nombre actual mydirectory en el directorio de trabajo.

Se prueba que el directorio con el nombre actual no exista, si existe le agrega al nombre actual la cadena _nx (de ‘next’). La prueba de existencia y extensión de nombre se iteran hasta que el nombre actual del directorio no exista, en ese momento se lanza la excepción. El manejo de la excepción consiste en crear el directorio inexistente con el nombre actual.

import os

newdir = r'./mydirectory'
while True:
    try:
        os.stat(newdir)
        # extending the name to make it unique
        newdir= newdir+'_nx'
    except:
        os.mkdir(newdir)
        break

Implementación de control de programa tipo do-while

Antes de crear el directorio, debemos revisar que éste no exista. Esto requiere de una implementación del tipo do-while.

Como en Python no existe el estatuto do-while, la estructura presentada puede ser base de su implementación.

while True:
    try:
        wait_here_until_condition_fails()
    except:
        once_conditions_fails_do_something_and_exit()
        break

De cumplimiento obligado: assert

Con assert se establecen las condiciones que se debe cumplir un momento determinado, en caso contratio se producirá una excepción AssertionError, que hay que atrapar y procesar adecuadamente

def factorial(n):
    try:
        assert (int(n)==n & n>=0)
        if n==0:
            return 1
        return n*factorial(n-1)
    except AssertionError:
        print('Error: factorial is only defined for non-negative integers')
        
F = factorial(-5)
print(F)
Error: factorial is only defined for non-negative integers
None

La forma corta, y simple, de assert es

assert expression, argument

Usando esta forma, el ejemplo del factorial queda:

def factorial(n):
    assert (int(n)==n & n>=0), 'Error: factorial is only defined for non-negative integers'
    if n==0:
        return 1
    return n*factorial(n-1)

Cuando invocamos a la función con un parámetro inválido, se genera una execpción. Si la excepción no es atrapada, tenemos un error de ejecución, como vemos a continuación:

F = factorial(-5)
print(F)
---------------------------------------------------------------------------

AssertionError                            Traceback (most recent call last)

<ipython-input-29-43eacdb6362f> in <module>()
----> 1 F = factorial(-5)
      2 print(F)


<ipython-input-27-38709b56c52e> in factorial(n)
      1 def factorial(n):
----> 2     assert (int(n)==n & n>=0), 'Error: factorial is only defined for non-negative integers'
      3     if n==0:
      4         return 1
      5     return n*factorial(n-1)


AssertionError: Error: factorial is only defined for non-negative integers

El mensaje de excepción atrapado en un nivel superior puede servir para depurar nuestro programa mediante mensajes o almacenarlo en un archivo .log; ver el módulo logging.

try:
    F = factorial(-5)
    print(F)
except AssertionError as error:
    print(error)
else:
    print('Fin sin excepción')
Error: factorial is only defined for non-negative integers

La ventaja de enviar los mensajes de excepción para que sean procesados a un nivel superior es que, generalmente, el programador de la función a la que es invocada (nivel de librería) desconoce la gravedad del contexto en que ocurre la excepción. Esto es, si el error es catastrófico o no. Por ello no es conveniente dejar a la librería la responsabilidad de terminar la ejecución de un programa que la invoca.

Por ejemplo, imaginemos que estamos en un programa interactivo y deseamos guardar los resultados parciales en archivo temporal, pero resulta que ya existe un archivo previo con el nombre que hemos elegido, por lo que no se pueded crear el nuevo archivo y se genera la excepción de “falla de crear archivo nuevo myfile.dat”. Esto no deberia ser razón para que la librería de manejo de archivos terminará la ejecución del programa, sino para tener la oportunidad de elegir otro nombre. Por supuesto que podemos encontrar un ejemplo en el que la falla de crear un archivo de salida sea catastrófica y debemos detener la ejecución. En cual de los dos casos estemos, dependerá del diseño del programa que usa la librería de manejo de archivos.