martes, 11 de enero de 2022

Tratamiento de las Excepciones en Python


imagen de código en python para tratar excepciones.







Excepciones.

Por definición las excepciones son errores que se producen durante la ejecución del código y que no son esperadas por el usuario.

Un ejemplo típico es el tener un programa que divide dos números y tratamos de dividir uno de ellos entre cero.

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

Para gestionarlas y que no finalicen de forma inesperada un programa, podemos usar las siguientes instrucciones de Python:


try: 
    [instrucciones o código a ejecutar.]
except <tipo de excepción>, <tipo de excepción>...:
    [instrucciones o código a ejecutar si ocurre la excepción señalada. En 
    el caso de que no se indique una excepción en concreto y se deje en
    blanco, la excepción se ejecutará con cualquier tipo de error. También
    se pueden poner varios except seguidos y saltará el código del error 
    correspondiente.]
except: # Si ponemos excepciones especificas, está última debe ser la 
    genérica para recoger las que no se recojan previamente. La genérica
    SIEMPRE DEBE SER LA ÚLTIMA en colocarse. 
else: # esta instrucción es opcional
    [código a ejecutar solamente si no ocurre ninguna excepción.]
finally: # este instrucción es opcional
    [código a ejecutar tanto si ocurren excepciones como si no.]

Un ejemplo. Si creamos una función que muestre el resultado de dividir un número entre otro:

def division(numero_1, numero_2):
    try:
        print(numero_1/numero_2)
    except ZeroDivisionError:
        print("No puedes dividir un número entre cero.")
        return "operación errónea"
    else:
        print("División realizada correctamente.")
    finally:
        return "Función terminada."

y le pasamos un resultado correcto por ejemplo dividir 8 entre 2:

print(division(8,4))
obtendremos el siguiente resultado:

2.0
División realizada correctamente.
Función terminada.
Si por el contrario intentamos dividir entre cero

print(division(8,0))
la excepción saltará y nos mostrará el siguiente resultado:

No puedes dividir un número entre cero.
Función terminada.

También podemos poner "finally" sin los "except" previos que capturan el error. En este caso primero se ejecutaría el código que hubiésemos definido dentro del "finally" y luego el programa mostraría el error. Si prescindiéramos del "finally", entonces el error impide que siga el programa. 

Por ejemplo:

try:
    print(8/0)
finally:
    print("Ha habido un error")
Ha habido un error
Traceback (most recent call last):
  File "<string>", line 2, in <module>
ZeroDivisionError: division by zero
> 
Como ves se muestra el mensaje, aunque previamente se ha producido un error en el programa.

Excepciones propias - RAISE -


La instrucción "raise" en Python permite lanzar un mensaje de error con un texto personalizado. Se suele poner después de la instrucción "except", aunque también lo puedes poner donde quieras para llamar al error.

La síntesis para lanzar el error sería:

raise TipodeError("Texto explicativo del error") 

Siguiendo con el ejemplo que hemos usado hasta ahora

# Vamos a dividir 8 entre un número pero no queremos que se pueda
# utilizar el cero como divisor.
divisor = int(input("Introduce un numero como divisor: "))
if divisor == 0:
    raise ZeroDivisionError("¡No se puede dividir entre cero!")
        
print(8/divisor)
Introduce un numero como divisor: 0
Traceback (most recent call last):
  File "<string>", line 5, in <module>
ZeroDivisionError: ¡No se puede dividir entre cero!
> 


La instrucción assert.


Aunque en si misma no sirve para detectar tratar directamente excepciones si que nos va a servir para detectar errores de código y para testear nuestro programa. La estructura es:

assert condición, "texto a mostrar"


Imaginemos que creamos una función que solo calcula la raíz cuadrada de un número. La raíz cuadrada de un numero es ese mismo número elevado a 1/2 o 0.5. Para saber si la hemos construido bien sabemos que la raíz cuadrada de 25 es 5.0 por tanto podemos probar la función usando assert de la siguiente manera para ver si la función funciona correctamente:

def raiz_cuadrada(numero):
    return pow(numero, 0.5)
    
resultado = raiz_cuadrada(25)
assert resultado == 5.0, "La función no funciona."
print("el calculo es correcto")
Si la condición es True no hace nada y el programa continua normalmente, pero por el contrario si la condición es False lanza un error "AssertionError" y detiene el programa. Si ejecutamos el código tal como está:


 el calculo es correcto
>
Si por ejemplo ahora cambiamos el exponente por que nos hemos confundido al teclear y ponemos 0.6 y volvemos a ejecutar el programa:

def raiz_cuadrada(numero):
    return pow(numero, 0.6)
    
resultado = raiz_cuadrada(25)
assert resultado == 5.0, "La función no funciona"

print("el calculo es correcto")
Traceback (most recent call last):
  File "<string>", line 5, in <module>
AssertionError: La función no funciona
Vemos que nos muestra que la función no funciona como debería y nos funciona una pista para ver donde esta el fallo y poder depurarlo.

Si ya has visto el tema de la programación orientada a objetos de Python puedes seguir leyendo esta ampliación sobre las excepciones.

Las excepciones son Clases.


Los ejemplos anteriores se centraron en detectar un tipo específico de excepción y responder de manera apropiada. Ahora vamos a profundizar más y mirar dentro de la excepción misma.

Probablemente no te sorprenderá saber que las excepciones son clases. Además, cuando se genera una excepción, se crea una instancia de un objeto de la clase y pasa por todos los niveles de ejecución del programa, buscando el bloque "except" que está preparado para tratar con la excepción.

Tal objeto lleva información útil que puede ayudarte a identificar con precisión todos los aspectos de la situación pendiente. Para lograr ese objetivo, Python ofrece una variante especial de la cláusula de excepción: puedes encontrarla en el siguiente código.

try:
    int("Hola")
except Exception as e:
    print(e)
    print(e.__str__())

Salida:

invalid literal for int() with base 10: 'Hola'
invalid literal for int() with base 10: 'Hola'


Como puedes ver, la sentencia except se extendió y contiene una frase adicional que comienza con la palabra clave reservada as, seguida por un identificador. El identificador está diseñado para capturar la excepción con el fin de analizar su naturaleza y sacar conclusiones adecuadas.

Nota: el alcance del identificador solo es dentro del except, y no va más allá.

El ejemplo presenta una forma muy simple de utilizar el objeto recibido: simplemente imprímelo (como puedes ver, la salida es producida por el método del objeto __str__()) y contiene un breve mensaje que describe la razón.

Se imprimirá el mismo mensaje si no hay un bloque except en el código, y Python se verá obligado a manejarlo por sí mismo.

Todas las excepciones de Python forman una jerarquia de clases. Si quieres puedes verlas sin problemas. Observa el código.

def imprimir_arbol_excepciones(estaclase, anidado = 0):
    if anidado > 1:
        print("   |" * (anidado - 1), end="")
    if anidado > 0:
        print("   +---", end="")
    
    print(estaclase.__name__)
    for subclass in estaclase.__subclasses__():
        imprimir_arbol_excepciones(subclass, anidado + 1)
        
imprimir_arbol_excepciones(BaseException)  
Salida:

BaseException
   +---Exception
   |   +---TypeError
   |   +---StopAsyncIteration
   |   +---StopIteration
   |   +---ImportError
   |   |   +---ModuleNotFoundError
   |   |   +---ZipImportError
   |   +---OSError
   |   |   +---ConnectionError
   |   |   |   +---BrokenPipeError
   |   |   |   +---ConnectionAbortedError
   |   |   |   +---ConnectionRefusedError
   |   |   |   +---ConnectionResetError
   |   |   +---BlockingIOError
   |   |   +---ChildProcessError
   |   |   +---FileExistsError
   |   |   +---FileNotFoundError
   |   |   +---IsADirectoryError
   |   |   +---NotADirectoryError
   |   |   +---InterruptedError
   |   |   +---PermissionError
   |   |   +---ProcessLookupError
   |   |   +---TimeoutError
   |   |   +---UnsupportedOperation
   |   |   +---ItimerError
   |   +---EOFError
   |   +---RuntimeError
   |   |   +---RecursionError
   |   |   +---NotImplementedError
   |   |   +---_DeadlockError
   |   +---NameError
   |   |   +---UnboundLocalError
   |   +---AttributeError
   |   +---SyntaxError
   |   |   +---IndentationError
   |   |   |   +---TabError
   |   +---LookupError
   |   |   +---IndexError
   |   |   +---KeyError
   |   |   +---CodecRegistryError
   |   +---ValueError
   |   |   +---UnicodeError
   |   |   |   +---UnicodeEncodeError
   |   |   |   +---UnicodeDecodeError
   |   |   |   +---UnicodeTranslateError
   |   |   +---UnsupportedOperation
   |   +---AssertionError
   |   +---ArithmeticError
   |   |   +---FloatingPointError
   |   |   +---OverflowError
   |   |   +---ZeroDivisionError
   |   +---SystemError
   |   |   +---CodecRegistryError
   |   +---ReferenceError
   |   +---MemoryError
   |   +---BufferError
   |   +---Warning
   |   |   +---UserWarning
   |   |   +---DeprecationWarning
   |   |   +---PendingDeprecationWarning
   |   |   +---SyntaxWarning
   |   |   +---RuntimeWarning
   |   |   +---FutureWarning
   |   |   +---ImportWarning
   |   |   +---UnicodeWarning
   |   |   +---BytesWarning
   |   |   +---ResourceWarning
   +---GeneratorExit
   +---SystemExit
   +---KeyboardInterrupt
Este programa muestra todas las excepciones predefinidas en forma de árbol.

Anatomía detallada de las excepciones.


La clase BaseException introduce una propiedad llamada args. Es una tupla diseñada para reunir todos los argumentos pasados al constructor de la clase. Está vacío si la construcción se ha invocado sin ningún argumento, o solo contiene un elemento cuando el constructor recibe un argumento y así sucesivamente (no se considera argumento aquí el self)

Vamos a ver un poco esto a través del siguiente código.

def print_args(args):
    lng = len(args)
    if lng == 0:
        print("")
    elif lng == 1:
        print(args[0])
    else:
        print(str(args))

try:
    raise Exception
except Exception as e:
    print(e, e.__str__(), sep = " : ", end = " : ")
    print_args(e.args)
    
try:
    raise Exception("Mi excepción")
except Exception as e:
    print(e, e.__str__(), sep = " : ", end = " : ")
    print_args(e.args)
    
try:
    raise Exception("Mi", "excepción")
except Exception as e:
    print(e, e.__str__(), sep = " : ", end = " : ")
    print_args(e.args)
Hemos utilizado la función para imprimir el contenido de la propiedad args en tres casos diferentes, donde la excepción de la clase Exception es generada de tres maneras distintas. Para hacerlo más espectacular, también hemos impreso el objeto en sí, junto con el resultado de la invocación __str__().

El primer caso parece de rutina, solo hay el nombre Exception después de la palabra clave reservada raise. Esto significa que el objeto de esta clase se ha creado de la manera más rutinaria.

El segundo y el tercer caso pueden parecer un poco extraños a primera vista, pero no hay nada extraño, son solo las invocaciones del constructor. En la segunda sentencia raise, el constructor se invoca con un argumento, y en el tercero, con dos.

Como puedes ver, la salida del programa refleja esto, mostrando los contenidos apropiados de la propiedad args:

 :  : 
Mi excepción : Mi excepción : Mi excepción
('Mi', 'excepción') : ('Mi', 'excepción') : ('Mi', 'excepción')

¿Cómo crear tu propia excepción?

La jerarquía de excepciones no está cerrada ni terminada, y siempre puedes ampliarla si deseas o necesitas crear tu propio mundo poblado con tus propias excepciones.

Puede ser útil cuando se crea un módulo complejo que detecta errores y genera excepciones, y deseas que las excepciones se distingan fácilmente de cualquier otra de Python.

Esto se puede hacer al definir tus propias excepciones como subclases derivadas de las predefinidas.

Nota: si deseas crear una excepción que se utilizará como un caso especializado de cualquier excepción incorporada, derívala solo de esta. Si deseas construir tu propia jerarquía, y no quieres que esté estrechamente conectada al árbol de excepciones de Python, derívala de cualquiera de las clases de excepción principales, tal como: Exception.

Imagina que has creado una aritmética completamente nueva, regida por sus propias leyes y teoremas. Está claro que la división también se ha redefinido, y tiene que comportarse de una manera diferente a la división de rutina. También está claro que esta nueva división debería plantear su propia excepción, diferente de la incorporada ZeroDivisionError, pero es razonable suponer que, en algunas circunstancias, tu (o el usuario de tu aritmética) pueden tratar todas las divisiones entre cero de la misma manera.

Demandas como estas pueden cumplirse en la forma presentada en siguiente código.

class MiDivisionEntreCeroError(ZeroDivisionError):
    pass

def haz_la_division(mine):
    if mine:
        raise MiDivisionEntreCeroError("Peores Noticias")
    else:
        raise ZeroDivisionError("Malas Noticias")
        
for modo in [False, True]:
    try:
        haz_la_division(modo)
    except ZeroDivisionError:
        print('División entre cero')
        
for modo in [False, True]:
    try:
        haz_la_division(modo)
    except MiDivisionEntreCeroError:
        print('Mi división entre cero')
    except ZeroDivisionError:
        print('División entre cero original')

Salida:

División entre cero
División entre cero
División entre cero original
Mi división entre cero
Mira el código y analicémoslo:

Hemos definido nuestra propia excepción, llamada MiDivisionEntreCeroError , derivada de la incorporada ZeroDivisionError. Como puedes ver, hemos decidido no agregar ningún componente nuevo a la clase.

En efecto, una excepción de esta clase puede ser, dependiendo del punto de vista deseado, tratada como una simple excepción ZeroDivisionError, o puede ser considerada por separado.

La función haz_la_division() genera una excepción MiDivisionEntreCeroError o ZeroDivisionError dependiendo del valor del argumento.

La función se invoca cuatro veces en total, mientras que las dos primeras invocaciones se manejan utilizando solo un bloque except (la más general), las dos últimas invocan dos bloques diferentes, capaces de distinguir las excepciones (no lo olvides: el orden de los bloques hace una diferencia fundamental).

Vamo ahora a crear un sistema de excepciones que no tienen nada que ver con las que vienen por defecto en Python. Imagina que estás trabajando en un sistema para simular una pizzeria. Sería conveniente crear un sistema de excepciones por separado a las de Python. 

Podemos empezar a construirla definiendo una excepción general como una nueva clase base para cualquier otra excepción generalizada. 

class PizzaError(Exception):
    def __init__(self, pizza, mensaje):
        Exception.__init__(self, mensaje)
        self.pizza = pizza
Vamos a recopilar mas información de la que recoge normalmente una Exception, por ello le pasamos dos argumentos:

- Uno que especifica una pizza determinada como tema del proceso.
- Otro que contiene una descripción más o menos precisa del problema.

El segundo parámetro se lo pasamos al constructor de la superclase y guardamos el primero dentro de nuestra propiedad.

Un problema más especifico en una pizza como puede ser un exceso de queso puede requerir una Excepción más especifica:

class DemasiadoQueso(PizzaError):
    def __init__(self, pizza, queso, mensaje)
    super().__init__(pizza, mensaje)
    self.queso = queso

La excepción DemasiadoQueso necesita más información que la excepción regular PizzaError.  Por esto lo agregamos al constructor y guardamos el nombre queso para su uso posterior.

Vamos a combinar las dos excepciones anteriores en un pequeño ejemplo:

class PizzaError(Exception):
    def __init__(self, pizza, mensaje):
        Exception.__init__(self, mensaje)
        self.pizza = pizza
        
class DemasiadoQueso(PizzaError):
    def __init__(self, pizza, queso, mensaje):
        super().__init__(pizza, mensaje)
        self.queso = queso
    
def haciendo_pizza(pizza, queso):
    if pizza not in ['margarita','romana', 'calzone']:
        raise PizzaError(pizza, "No existe tal pizza en el menú.")
    if queso > 100:
        raise DemasiadoQueso(pizza, queso, "Demasiado queso en la pizza.")
    print("¡La pizza está lista!.")
        
for (pz, qs) in [('calzone', 10), ('margarita', 110), ('cazurra',20)]:
    try:
        haciendo_pizza(pz, qs)
    except DemasiadoQueso as dmqs:
        print(dmqs, ':', dmqs.queso)
    except PizzaError as pe:
        print(pe, ':', pe.pizza)
Salida:

¡La pizza está lista!.
Demasiado queso en la pizza. : 110
No existe tal pizza en el menú. : cazurra

Una de las excepciones es generada dentro de la función haciendo_pizza() cuando ocurra alguna de estas situaciones erróneas:

- Una pizza con demasiado queso.
- Una pizza que no está en la carta.

Nota:
  • Si removemos el bloque except DemasiadoQueso, todas las excepciones que se produzcan serán PizzaError.
  • El remover el bloque que comienza con except PizzaError provocará que la excepción DemasiadoQueso no pueda ser manejada, y hará que el programa finalize.
Aunque la solución anterior funciona se necesita pasar los argumentos requeridos o sino las excepciones no funcionan. Esto se puede solucionar estableciendo valores predeterminados para todos los parámetros del constructor.

La solución sería:

class PizzaError(Exception):
    def __init__(self, pizza = 'desconocida', mensaje = ''):
        Exception.__init__(self, mensaje)
        self.pizza = pizza
        
class DemasiadoQueso(PizzaError):
    def __init__(self, pizza = 'desconocida', queso = '>100', mensaje = ''):
        super().__init__(pizza, mensaje)
        self.queso = queso
    
def haciendo_pizza(pizza, queso):
    if pizza not in ['margarita','romana', 'calzone']:
        raise PizzaError(pizza, "No existe tal pizza en el menú.")
    if queso > 100:
        raise DemasiadoQueso(pizza, queso, "Demasiado queso en la pizza.")
    print("¡La pizza está lista!.")
        
for (pz, qs) in [('calzone', 10), ('margarita', 110), ('cazurra',20)]:
    try:
        haciendo_pizza(pz, qs)
    except DemasiadoQueso as dmqs:
        print(dmqs, ':', dmqs.queso)
    except PizzaError as pe:
        print(pe, ':', pe.pizza)









No hay comentarios:

Publicar un comentario