TRATAMIENTO DE EXCEPCIONES

VISTA DEL TEIDE DESDE EL MONTEVERDE DE LAS LADERAS DE ACENTEJO, CENTRO NORTE DE TENERIFE.

      Trabajaremos en este apartado, un tema singular, común a todos los lenguajes de programación del mundo mundial, pero que en Python, como no puede ser de otra forma, 😏, cuenta con sus propias peculiaridades. ¡Venga!, manos a la obra.
Por así decirlo, refiere al control por parte del programador de las Excepciones que, en resumidas cuentas, no es otra cosa, como ya sabemos, que un aviso de error, bien durante la ejecución de un programa, o bien mientras lo estamos diseñando (errores de sintaxis, incompatibilidad de operadores entre tipos de datos, error de indentación, variables indefinidas, etc. como los más habituales cuando empezamos a programar), junto a determinadas  condiciones excepcionales, y de aquí lo de Exception, que puedan devenir en el transcurso de su diseño o en el desarrollo normal del flujo de lectura.
Constituye en sí mismo un mecanismo que Python pone a nuestra disposición con el propósito de que podamos prever, (y actuar en consecuencia), determinados eventos de programación susceptibles de generar un error y, a modo de prevención, modificar el flujo de ejecución del programa.
De facto, las excepciones surgen con la Programación Orientada a Objetos, la POO, que estudiaremos un poco más adelante, y consiste en un tipo de objeto especial: el objeto ERROR, que sólo se ejecuta en situaciones concretas y bajo ciertas condiciones.
Cuando se advierte un error, aún a pesar de que la sintaxis fuera del todo correcta y limpia, en lo que se denomina "tiempo de ejecución" de un programa, y éste es detectado por el intérprete de Python, se produce (segurísimo que ya nos ha sucedido en un buen puñado de ocasiones en nuestros testeos, o tratando  de resolver alguno de los ejercicios que hemos propuesto. ¡Ajjj! El dichoso mensaje de error con su rojo chillón de marras) un aborto automático de la ejecución del programa a la vez que se lanza un evento de error. Esto es lo que se denomina raise, que tiene valor de sentencia en Python como veremos a continuación. El control o gestión de excepciones nos da la opción comprobar si se ha lanzado uno de estos eventos y determinar su naturaleza.

PÍJARAS (HELECHOS) EN EL BOSQUE HÚMEDO DE ANAGA DE BARLOVENTO, MACIZO DE ANAGA, NORESTE DE TENERIFE


      RAISE:

      
     Debemos tener en cuenta que todas las Excepciones (raise) posibles, tanto las preconstruidas como las que podamos construir nosotros, derivan de una única clase predefinida en Python: la clase BaseException, que devuelve un objeto especial de tipo ERROR, como anunciamos más arriba que interrumpe  automáticamente la ejecución de un programa, e informa del motivo de su lanzamiento, de modo que facilita al programador la localización del "desliz" pudiendo incluir, además, cierta codificación alternativa que nos faculte para gestionar el error.




Podemos añadir que la más común de las excepciones, estadísticamente hablando, es el error de sintaxis, el objeto de tipo ERROR preconstruido SintaxError (observemos que muchas de las excepciones terminan con la palabra 'Error', para recordarnos que se ha generado un objeto de tipo ERROR por si no comemos suficiente fósforo), que constituye en sí mismo un tipo de excepción particular ya que no podemos manejarlo como sí es posible hacerlo en otro tipo de excepciones, lo que nos obliga a reescribir correctamente la línea de código desde donde se lanzó la excepción. Otro error muy frecuente es un fallo en la indentación, en el sangrado, de acuerdo a la estructura propia del lenguaje de Python, lanzándose en este caso un objeto IndentationError.
En el siguiente ejemplo mostramos unos pocos tipos de excepciones:


Contamos con la sentencia raise que procura el lanzamiento, controlado, claro, de una excepción, precediendo a un objeto ERROR por su nombre (TypeError, ZeroDivisionError, AttributeError, ValueError, IndexError, etc.) que será el invocado por raise. Observemos que estos objetos de tipo ERROR que hemos pasado como ejemplo son todos preconstruidos, built-in, pero es perfectamente posible colocar en su lugar un objeto de tipo ERROR, definido por el usuario, con un componente y función similares.


También podemos recurrir a la clase Exception de forma genérica, por ejemplo, si no tenemos claro el tipo de error que pueda sobrevenir:



Es posible, incluso, hacer cosas a pesar del error, independientemente de que Python lance una excepción de TypeError avisándonos de que el objeto ERROR debe derivar de BaseException.


Combinamos dos funciones def donde la segunda llama a la primera, observemos el resultado que obtenemos.



CHUMBERAS/PENCAS Y SU FRUTO EN CUALQUIER PARTE DEL SUR DE TENERIFE.

      MANEJO DE EXCEPCIONES:

      

      🔲 CAPTURA Y PRODUCCIÓN DE EXCEPCIONES: 


      Para explicar este nuevo concepto lo mejor es comenzar con un ejemplo. Vamos a provocar un error simplón al intentar acceder a un elemento de una lista recurriendo a un índice que marca un número similar al length (len()) de la lista. Recordemos que el número total de índices llamables (callables) dentro de una lista viene determinado por el logaritmo len() - 1:


Si suponemos que estas dos líneas que acabamos de codificar formasen parte de un programa mucho mayor (por ejemplo, uno que mostrase una lista de alimentos por categoría tolerados por personas hiperglucémicas que, al seleccionar cada elemento de las distintas listas, nos mostrase una breve compilación de las principales características nutricionales de cada uno), automáticamente, en el momento mismo de lanzarse la excepción que hemos visto se interrumpe la ejecución del programa.


En este ejemplo, podemos ver que la última línea del texto nos indica el tipo de error o excepción que se ha producido (IndexError, error de indexado o de índice) y su especificación o aclaración (list index out of range, el índice de la lista que hemos solicitado se sale del rango de índices que posee dicha lista).
Como acabamos de mencionar, una excepción de este tipo detiene de manera automática la ejecución del programa. ¿Cómo podemos evitar esto? ¿Cómo podemos "capturar" la excepción IndexError que nos ha  lanzado el intérprete de Python en el ejemplo precedente?
Pues lo hacemos con los bloques de código asociados a las cláusulas try/except. ¡Tachán! No hace falta mencionar que try remite a "probar" (to try, probar) y except, que remite a la "excepción", al "error", que podría ocurrir si algo fallara en el código que estamos probando, por lo que ya nos podemos ir haciendo una idea de por dónde van los tiros.
Obviamente, para capturar una excepción, la cláusula try es obligatoria, mientras que siempre tiene que haber, como mínimo, un bloque except (Sí, pueden haber más de uno).
Veamos su sintaxis en el siguiente esquema:


CARDONCILLO PLATEADO, PLANTA ENDÉMICA DE LA FLORA DE ALTURA EN LAS CAÑADAS DEL TEIDE, TENERIFE.

Vamos a aplicar a continuación este esquema a nuestro ejemplo:



En 1. introducimos la función integrada print() e incluimos como argumento la línea de código que ha lanzado la excepción: lista_frutas[4]. En 2. escribimos, después de la cláusula except, la identificación de la excepción que ha lanzado el intérprete de Python y que se corresponde con el identificador nombrado con la sintaxis camelKey, que aparece en la última línea del texto de la misma, antes de los dos puntos, en nuestro caso, IndexError. En el caso de que no se identificara ninguna, se procedería a capturar cualquier excepción.
En el ejemplo que viene a continuación tenemos una "captura de error" antes de producirse o darse éste, pues tan sólo nos muestra una advertencia como programadores, los padres de la criatura.


Esta estructura no interrumpe la ejecución del script que estamos desarrollando (hasta donde sea posible; claro: si el error impide proporcionar un dato clave para proseguir con la ejecución del código, el programa se aborta: Python no hace magia 🍀🍀, ni tampoco toma decisiones por sí mismo). Tras la cláusula except debemos señalar el tipo de excepción que el intérprete de Python debe capturar. En el caso de que no consiguiéramos ninguno, el intérprete de Python capturará cualquier excepción.
Es importante saber que a partir de la inclusión de una sentencia try se tendrá en cuenta cualquier línea de código. Para entendernos, una vez que se activa el radar de velocidad de una autopista, éste evaluará la velocidad de todos los automóviles que crucen frente a él a partir de ese momento, independientemente de si éstos son coches, motocicletas, camiones, furgonetas, monovolúmenes, tanquetas del ejército, el correcaminos delante del coyote, etc.. En el momento en que una de estas líneas de código cause una excepción pasará a ejecutarse automáticamente el bloque except.
Ahondando en la sintaxis que mostramos en el esquema correspondiente, como hemos dicho ya, en paralelo a una sentencia try debe haber, como mínimo, una cláusula except con su bloque correspondiente, pero las cláusulas else y finally son optativas, y su finalidad pasa por perfilar mejor  la cadena de la excepción.
El bloque else se ejecuta cuando termina la ejecución del bloque try aunque, si se produce la excepción, obviamente, no se ejecutará. En el caso de que hayamos introducido una cláusula finally, con su bloque correspondiente, como su nombre sugiere, se ejecutará al final del ciclo.
Veámoslo a continuación:


Ya hemos comentado que puede haber más de un bloque except. En este caso, podemos recurrir a una tupla de excepciones, cómo no, separadas por comas:


La plasticidad de Python nos permite asignar la excepción a una variable. En este caso, la variable almacenará la excepción que le hemos señalado pudiendo acceder posteriormente a ella desde el bloque de excepción, lo que nos puede resultar más cómodo y útil si vamos a trabajar con muchas excepciones de un mismo tipo.


Apuntamos a lo que debería hacer nuestro código para "recuperarse" de una excepción para poder proseguir, siempre que fuera posible, con su ejecución.
Para ello contamos con la opinión de envolver, rodear, circundar cualquier script o trozo de código susceptible de lanzar una excepción en un momento dado dentro de una cláusula try/except. Veamos un ejemplo de sintaxis básica:



La aplicación de una función definida devuelve el siguiente resultado:


Una vez "atrapada" una excepción, cualquiera que sea, mediante la fórmula raise Exception(texto que se quiera introducir), ya es posible manejarla, manipularla, a nuestro criterio mediante el recurso al bloque try/except.
Como acabamos de ver en el esquema, el código que va a manejar la cláusula except debe situarse debajo, debidamente indentado. Tengamos en cuenta que la clase Exception no diferencia entre los distintos tipos de excepciones que reconoce Python, es decir, aquéllas que vienen predeterminadas dentro de BaseException, pero es posible "adosar" a except el tipo de excepción que esperamos recibir a través de la sintaxis siguiente:


Si se produjera un error distinto al esperado se lanzaría en su lugar la excepción clásica prevista de Python sin ningún problema. Incluso podemos ampliar nuestras opciones manejando errores tal y como se muestra en el ejemplo que proponemos a continuación:


No existe en Python un límite preciso para adicionar excepciones a nuestro bloque try/except. De hecho, podemos incrustar los que queramos, cada uno con su propio bloque de código. Además, podemos incluir la cláusula raise, eso sí, sin pasarle argumento alguno, al final del bloque para que nos reintegre a la función;


Contamos con la opción de tratar una excepción como una variable con todas las ventajas que ello supone desde el punto de vista programático. Para hacerlo, tan sólo tenemos que recurrir a la cláusula as, generadora de alias, y facultándonos así para acceder a los diferentes atributos característicos de las excepciones:



   

      EN ALGUNAS OCASIONES, BÁSICAMENTE CUANDO TENEMOS INPUTS (ENTRADAS DE DATOS), ES UNA BUENA IDEA ENVOLVER UN BLOQUE try/except EN UN BUCLE INFINITO, ESTO ES, while True:, PARA QUE, EN CASO DE SUSCITARSE UN ERROR, NOS DEVUELVA A LA PRIMERA LÍNEA DEL BLOQUE try/except.




Finalizamos aquí este apartado y pasamos a centrarnos en su jerarquía.

TRONCO DE HIJA ABOVEDADO SOBRE UN SENDERO EN LA LAURISILVA PROFUNDA DEL MACIZO DE ANAGA.
Llegados aquí, conviene hacer una advertencia para quienes codificamos "con prisas": informática viene de información. Y cuanto mejor organicemos y manejemos esa información, mejores serán también nuestros resultados y el producto final de nuestro trabajo. Podemos sentirnos tentados por construir la siguiente estructura de tratamiento de excepciones si no nos importa tanto identificar (y resolver convenientemente el problema) como que nuestro código continúe funcionando, aunque sea a trancas y barrancas, con tal de que en la ejecución devuelva el resultado que esperamos y nos pongamos la mar de contentos:



Esto es, rellenar el ámbito de except de un bloque try/except con una sentencia pass y a otra cosa, mariposa.
Pues mejor que no. ¿Y por qué si parece tan traído a mano? Pues porque si no identificamos el error, sobre todo en scripts y programas complejos, que son los realmente interesantes, estaremos hurtando información para quienes revisen nuestro código y un flaco favor, como colaboradores nuestros, o programadores que se presten a contribuir a nuestro proyecto en una distribución open source, para programadores que tengan la obligación profesional de sostener nuestro código en un ámbito laboral, o hasta para nosotros mismos cuando, pasado el tiempo, revisemos nuestro propio programa. Como no conocemos el error, si nuestro programa falla y no hemos insertado las medidas oportunas que nos brinda el uso de un bloque try/except, ¿cómo sabremos qué es lo que falla? ¿Dónde está el error de marras? ¿Cómo podremos corregirlo? ¡Ay mi cabeza! Vemos un ejemplo sencillo:



JERARQUÍA DE EXCEPCIONES



      Aunque este concepto corresponde más bien a la Programación Orientada a Objetos, la POO por sus siglas en español, que es el paradigma de programación que estudiaremos cuando entremos en Python a un nivel medio, podemos adelantar aquí un poco de su estructura y funcionamiento, a nivel teórico, para que "probemos un sorbito", por así decirlo.
Cuando el intérprete de Python, como ya sabemos, detecta una excepción en el bloque try, se dirigirá a la (o a las) cláusulas except. Y es aquí donde entronca el concepto de jerarquía de excepciones: si la excepción que hemos propuesto coincide con el tipo de excepción que detecta, el intérprete de Python procederá a la ejecución del bloque except que hayamos codificado. En caso de que no fuera así, pasará a la siguiente cláusula except donde hayamos insertado otra excepción o grupo de excepción (si lo hemos dispuesto dentro de una tupla como podemos ver en un ejemplo anterior, tan sólo pasa al siguiente elemento de la colección) para comprobar que concuerde con el que ha detectado y, en caso de ser así, ejecutar el bloque de código correspondiente.
¿Y cuál es esta jerarquía de excepciones? Pues de forma sintética, la que mostramos a continuación:



Y ahora ilustrémonos con un ejemplo sencillo que hemos tomado de Python 3, de Mark Summerfield, ed. Anaya Multimedia.
Imaginemos que el intérprete localiza una excepción KeyError en una búsqueda que le solicitamos hacer sobre un diccionario, por ejemplo, le hemos pedido que extraiga la clave Dictionary[10] cuando nuestro diccionario Dictionary sólo contiene cuatro elementos (pares clave/valor). Nuestro intérprete de Python sufre un shock nervioso y farfulla desesperado, enfebrecido, sufre jadeos electrónicos y sudoraciones binarias: Ha encontrado un KeyError.
Pues bien, ante tremenda tesitura, comprobará si el tipo de error que ha detectado él solito coincide o concuerda con la primera cláusula except que hemos propuesto. Teniendo en cuenta que la "lectura jerárquica" se hace de arriba a abajo y no al revés, el intérprete intentará concordar con la primera cláusula except de entre todas las que hayamos propuesto que contenga la clase Exception, dado que la excepción KeyError es una subclase de Exception (por debajo de ella y dependiente, pues, de ella en la estructura jerárquica. v. esquema). Si no lo encuentra, buscará la concordancia con cualquier except que contenga la clase LookupError, dado que KeyError es una subclase de la anterior. Si no la encuentra, buscará la concordancia con aquél que contenga la excepción KeyError. Y si la encuentra, la ejecutará.
Siempre que sea posible nos vendrá mejor optar por las excepciones más específicas porque nos ayudarán a controlar mejor nuestro programa. Dicho en plata: mejor un except con KeyError que un except con LookupError Exception. 
Por otra parte, también podemos escribir except: en nuestro script sin añadir bloque de código alguno, en cuyo caso, capturará cualquier excepción que ocurra. Por su generalidad y escasa definición es propensa a ocultar errores más especializados y que nos convendría detectar. Sucede lo mismo que introducir un tipo de error Exception, igualmente genérico, en nuestro except:. Como norma general, cuando utilizamos varios bloques except tenemos que jerarquizarlos desde el más específico (la jerarquía de menor nivel, en nuestro ejemplo, KeyError) al más genérico (la jerarquía de mayor nivel, en nuestro caso, Exception).
En el caso de que el intérprete de Python no encuentre ningún bloque except que concuerde con la excepción que ha detectado, el programa continuará con su ejecución tratando de localizar un controlador de excepciones que se ajuste a su parámetro de búsqueda. Finalmente, de no hacerlo, el programa se interrumpirá imprimiendo en el shell de nuestro IDLE el consabido traceback de marras.
Recordemos que si no se producen excepciones en nuestro código y hemos incluido en nuestro manejador de excepciones un bloque else, éste se ejecutará. Y de todas formas lo hará también un bloque finally que hayamos insertado al lado del manejador: los bloques finally se ejecutan sí o sí.

SENDERO Y TALUDES BARBADOS DE MUSGO Y LÍQUENES EN EL MACIZO DE ANAGA, NORESTE DE TENERIFE.
Es el momento oportuno para consultar una nueva entrada directamente ligada al capítulo actual de la excepciones: T2. ASSERT. AFIRMACIONES: DIME LA VERDAD.

4 comentarios: