class: center, middle, inverse, title-slide .title[ # Python II y GitHub ] .institute[ ### Licenciatura en Ciencias Genómicas,UNAM ] .date[ ### First version: yyy-mm-dd; Last update: 2024-04-11 ] --- <style type="text/css"> /* From https://github.com/yihui/xaringan/issues/147 */ .scroll-output { height: 80%; overflow-y: scroll; } /* https://stackoverflow.com/questions/50919104/horizontally-scrollable-output-on-xaringan-slides */ pre { max-width: 100%; overflow-x: scroll; } </style> # Desarrollo de software y Errores Los errores son inevitables (*como todo en la vida, !eres humano!*) a la hora de desarrollar un programa, hasta el punto que la **estrategia más eficaz** para prevenirlos no es tanto evitar que ocurran, sino **intentar detectarlos y corregirlos rápidamente**. <img src="https://files.realpython.com/media/Python_Exceptions_Watermark.47f814fbeced.jpg" width="600px" style="display: block; margin: auto;" /> .tiny[Fuente: https://files.realpython.com/media/Python_Exceptions_Watermark.47f814fbeced.jpg] --- # Desarrollo de software y Errores Los errores de programación responden a diferentes tipos y pueden clasificarse dependiendo de la fase en que se presenten. Algunos tipos de errores son más difíciles de detectar y reparar que otros. <img src="https://files.realpython.com/media/Understanding-Pythons-Traceback_Watermarked.138741dabfeb.jpg" width="600px" style="display: block; margin: auto;" /> --- ## Tipos de errores Algunos tipos de errores: - errores de sintaxis. - errores lógicos o de diseño. - errores de tiempo de ejecución. --- # Errores de sintáxis Son errores en el **código**, en la Fase de Desarrollo. Son el resultado de **sintaxis incorrecta** (nombres de funciones mal escritas, incorrecta asignación de datos, etc). Los errores de sintaxis son fácilmente identificables cuando se ejecuta el programa, porque se mandará un mensaje en la pantalla con el error. El programa **no funcionará sino se resuelven**: -- ``` >>> while True print('Hello world') File "<stdin>", line 1 while True print('Hello world') ^ SyntaxError: invalid syntax ``` .content-box-green[ El intérprete reproduce la línea responsable del error y muestra una pequeña “flecha” que apunta al primer lugar donde se detectó el error. ] --- # Errores de sintáxis ``` >>> print(0 / 0)) File "<stdin>", line 1 print(0 / 0)) ^ SyntaxError: unmatched ')' ``` -- **Cómo los solucionamos?** El intérprete nos ayuda a encontrarlos. Ver a donde apunta la felcha y verificar cual fue el error de dedo o de sintáxis. Sino sabemos cual es el error, podemos: - **VSC**: nos marcará en rojo el caracter incorrecto. - **Usar chatgpt**: pegar el error y nos describirá el porque es incorrecto. - **google**: buscar páginas del error y ver la sintáxis. --- # Errores lógicos Los errores lógicos, son el resultado de una **incorrecta implementación lógica**. Si nos hemos equivocado al diseñar nuestro algoritmo, no habrá ningún programa que nos pueda ayudar a corregirlos. Un ejemplo sería un programa accediendo a una lista no ordenada suponiendo que lo estuviera. Los errores lógicos son los más difíciles de rastrear: - Un error que hace que una aplicación produzca resultados incorrectos. - Puede no generar un mensaje de error. -- ** ¿Cómo los solucionamos? ** .content-box-green[ Contra estos errores sólo cabe practicar y pensar, realizar pruebas, hacerle seguimiento y depuración a la aplicación hasta dar con el problema ( ver documento de análisis y diseño y los casos de prueba). ] --- # Errores lógicos Existen *bugs* en el código que no arrojan error y el código se ejecuta hasta el final **PERO** no está bien. -- ```python dna = 'atctgcatattgcgtctgatg' a_count = dna.count('A') # no va a encontrar A's ``` Cual es el error? -- **algun otro? ** --- # Errores en tiempo de ejecución Los errores en tiempo de ejecución son los errores más interesantes y comunes. Un ejemplo sería tratar de acceder a un archivo inexistente, o hacer una división con cero. ``` >>> 10 * (1/0) Traceback (most recent call last): File "<stdin>", line 1, in <module> ZeroDivisionError: division by zero ``` --- # Errores en tiempo de ejecución Por ejemplo, cuando no encontramos un archivo. Esto depende de nuestros *PATHS*. El código hace su tarea corre sin errores, mientras que en mi computadora arroja error si no cambio el nombre del archivo. ```python IOError: [Errno 2] No such file or directory: 'missing.txt' ``` -- .content-box-green[ A estas situaciones se les conoce como *exceptions* o *excepciones* y al control de ellas como *exception handling* o *manejo de excepciones*. ] **Como identificarlos?**: - Podemos anticiparnos a través de los casos de prueba (definimos el comportamiento del programa dada una situación) - Al encontrar un error de ejecución, detectar cual es el problema e implementar la excepción. --- # Manejo de Excepciones <img src="https://files.realpython.com/media/Raising-Exceptions-Manually-in-Python.-Best-Practices_Watermarked.ef77e2f3902b.jpg" width="600px" style="display: block; margin: auto;" /> --- # Manejo de excepciones Python maneja el **no encontrar un archivo** (`IOError: [Errno 2] No such file or directory`) devolviendo un _mensaje_ y deteniendo la ejecución del código (útil para el programador, no tanto para el usuario). -- Si sabemos que **leer un archivo** puede causar problemas, podemos identificarlo y agregar un mensaje para el usuario (te lo agradecerán mucho!). ```python try: f = open('misssing.txt') print('file contents: ' + f.read()) except: print("sorry, couldn't find the file") ``` --- # Manejo de excepciones `try` y `except` funcionan igual que `for/if` pero ayudan con la legibildad del código y [ligeramente más rápido](https://www.geeksforgeeks.org/try-except-vs-if-in-python/). Utilizar *manejo de excepciones* previene que Python rompa el código y permite que siga. -- - Sin *manejo de excepciones* ```python f = open('misssing') print('file contents: ' + f.read()) print("continuing....") ``` - Con *manejo de excepciones* ```python try: f = open('misssing') print('file contents: ' + f.read()) except: print("sorry, couldn't find the file") print("continuing....") ``` --- # Manejo de excepciones específicas Es importante reconocer que el código en `try` aplicará `except` siempre que haya **UN** error (el que sea). -- Asumamos que `data/7-file_num.txt` tiene números: ```python try: f = open('data/7-file_num.txt') my_number = int(f.read()) print(my_number + 5) # sumar except: print("sorry, couldn't find the file") ``` -- Intentar `data/7-file_num.txt` con texto. ¿Qué error/mensaje te da? -- <br><br> .center[.content-box-green[**mm pero si existe el archivo!!**]] --- # Manejo de excepciones específicas Con el ejemplo anterior podemos tener dos tipos de errores: 1. `IOError`: cuando no encuentra el archivo 2. `ValueError`: cuando intenta convertir texto en númerico. -- Podemos manejar los excepciones indicando especificamente cual es la *excepción*. ```python try: f = open('data/7-file_num.txt') my_number = int(f.read()) print(my_number + 5) except IOError: print("sorry, couldn't find the file") ``` --- # Manejo de excepciones específicas Podemos especificar ambos tipos de excepciones y controlar mensajes para cada caso. ```python try: f = open('data/7-file_num.txt') my_number = int(f.read()) print(my_number + 5) except IOError: print("sorry, couldn't find the file") except ValueError: print("sorry, couldn't parse the number") ``` -- Podemos manejar varios excepciones a la vez. ```python try: f = open('data/7-file_num.txt') my_number = int(f.read()) print(my_number + 5) except (IOError, ValueError): print("sorry, something went wrong") ``` --- ## Jerarquía de excepciones El manejo de excepciones se produce en función de una jerarquía de excepciones. En Python 3.x 3.0 ``` BaseException +-- SystemExit +-- KeyboardInterrupt +-- GeneratorExit +-- Exception +-- StopIteration +-- StopAsyncIteration +-- ArithmeticError | +-- FloatingPointError | +-- OverflowError | +-- ZeroDivisionError +-- AssertionError +-- AttributeError +-- BufferError +-- EOFError +-- ImportError --- ``` +-- LookupError | +-- IndexError | +-- KeyError +-- MemoryError +-- NameError | +-- UnboundLocalError +-- OSError | +-- BlockingIOError | +-- ChildProcessError | +-- ConnectionError | | +-- BrokenPipeError | | +-- ConnectionAbortedError | | +-- ConnectionRefusedError | | +-- ConnectionResetError | +-- FileExistsError | +-- FileNotFoundError ``` Source: https://learntutorials.net/es/python/topic/1788/excepciones --- # Obtener información de la excepción Podemos obtener información de la excepción Por ejemplo `IOError` aparecerá cuando no existe el archivo o no podemos abrirlo por permisos por lo que sería útil saber cual es la excepción. ```python try: f = open('data/7-file_num.txt') my_number = int(f.read()) print(my_number + 5) except IOError as ex: print("sorry, couldn't open the file: " + ex.strerror) except ValueError: print("sorry, couldn't parse the number") ``` --- # Obtener información de la excepción Los objetos `exception` (errores o excepciones) tienen un campo llamado `args`. ```python try: f = open('data/7-file_num.txt') my_number = int(f.read()) print(my_number + 5) except IOError as ex: print("sorry, couldn't find the file: " + ex.strerror) except ValueError as ex: print("sorry, couldn't parse the number: " + ex.args[0]) ``` --- # `else` en manejo de excepciones Si existieran más excepciones pero solo estamos interesados en `IOError` y `ValueError` para agregar un mensaje, podemos usar un `else` para que se ejecute `print(my_number + 5)` solo en caso de que no haya más errores. Sirve cuando necesitamos asegurarnos que no habrá ningún error. ```python try: f = open('data/7-file_num.txt') my_number = int(f.read()) except IOError as ex: print("sorry, couldn't find the file: " + ex.strerror) except ValueError as ex: print("sorry, couldn't parse the number: " + ex.args[0]) else: print(my_number + 5) ``` --- # `finally` en manejo de excepciones Podemos agregar `finally` para correr una linea de código sin importar si hubo excepción. Estas lineas se correran en todos los casos. Supongamos que tenemos un archivo temporal: ```python import os t = open('data/temp.txt', 'w') t.write('8') t.close() try: f = open('data/temp.txt') my_number = int(f.read()) print(my_number + 5) os.remove('data/temp.txt') # delete the temp file except IOError as ex: print("sorry, couldn't find the file: " + ex.strerror) os.remove('data/temp.txt') # delete the temp file except ValueError as ex: print("sorry, couldn't parse the number: " + ex.args[0]) os.remove('data/temp.txt') # delete the temp file ``` ¡Tenemos código repetido! --- # `finally` en manejo de excepciones Podemos sacarlo de `try` pero si existe algún otro error que no contemplamos, no sé borrará el archivo temporal. ```python import os t = open('data/temp.txt', 'w') t.write('8') t.close() try: f = open('data/temp.txt') my_number = int(f.read()) print(my_number + 5) except IOError as ex: print("sorry, couldn't find the file: " + ex.strerror) except ValueError as ex: print("sorry, couldn't parse the number: " + ex.args[0]) os.remove('data/temp.txt') # delete the temp file ``` --- # `finally` en manejo de excepciones Podemos agregar `finally` para eliminar el archivo `data/temp.txt` sin importar si hay errores. ```python import os t = open('data/temp.txt', 'w') t.write('8') t.close() try: f = open('data/temp.txt') my_number = int(f.read()) print(my_number + 5) except IOError as ex: print("sorry, couldn't find the file: " + ex.strerror) except ValueError as ex: print("sorry, couldn't parse the number: " + ex.args[0]) finally: os.remove('data/temp.txt') # delete the temp file ``` --- # Sintáxis para el manejo de excepciones <img src="https://www.learnbyexample.org/wp-content/uploads/python/Python-Exception-try-except-else-finally-Syntax.png" width="600px" style="display: block; margin: auto;" /> --- # Sintáxis para el manejo de excepciones Podemos tener está sintaxis: ```python try: # el código aquí se ejecutará hasta que se genere una excepción except ExceptionTypeOne: # código aquí se ejecutará si hay una excepción ExceptionTypeOne # está dentro del bloque try except ExceptionTypeTwo: # código aquí se ejecutará si hay una excepción ExceptionTypeTwo # está dentro del bloque try else: # el código aquí se ejecutará después del bloque try # si no genera una excepción finally: # el código aquí siempre se ejecutará ``` --- # `try` anidados Al igual que `if`, podemos anidar `try`. Considera que queremos cerrar nuestro archivo a pesar de que surgan errores ```python try: f = open('data/7-file_num.txt'') # this line might raise an IOError my_number = int(f.read()) # this line might raise a ValueError except IOError: print('cannot open file!') except ValueError: print('not an integer!') finally: f.close() ``` Esté código no cerrará porque `f` está definido dentro del bloque de `try` ( será variable local). --- # `try` anidados Podemos anidar `try`. ```python try: f = open('data/7-file_num.txt') try: my_number = int(f.read()) except ValueError: print('not an integer!') finally: f.close() print("the file was closed") except IOError: print('cannot open file') ``` --- # Manejo de excepciones Se recomienda el manejo de excepciones cuando se puede **hacer algo** al respecto y lo podemos hacerlo con `try/except`. Pero para detectar *bugs* necesitamos evocar/generar un error y poder *hacer algo*. Para ello Python usa `raise`. --- # `raise` `raise` nos permite generar un error en nuestro código (ayuda a identificar *bugs*). -- Podemos generar una variable de tipo clase de error (`ValueError`) y evocar el error con `raise`. ```python e = ValueError("this is a description of the problem") raise e ``` -- Otra sintaxis (más común): ```python raise ValueError() ``` --- Supongamos nuestra función `get_at_content`. ```python def get_at_content(dna): length = len(dna) a_count = dna.count('A') t_count = dna.count('T') at_content = (a_count + t_count) / length return at_content ``` -- Está función está diseñada cuando `dna` son `ATGC` y podemos generar un `ValueError` cuando sean `N`s. ```python def get_at_content(dna): if dna.count("N") > 0: raise ValueError(f'Sequence contains {dna.count("N")} N\'s') length = len(dna) a_count = dna.count('A') t_count = dna.count('T') at_content = (a_count + t_count) / length return at_content print(get_at_content('ACGTACGTGAC')) print(get_at_content('ACTGCTNAACT')) ``` --- # Manejo de errores Podemos ahora incorporar `try/except` para **hacer algo** cuando tengamos un error en nuestro código. ```python sequences = ['ACGTACGTGAC', 'ACTGCTNAACT', 'ATGGCGCTAGC'] for seq in sequences: try: print('AT content for ' + seq + ' is ' + str(get_at_content(seq))) except ValueError as ex: print('skipping invalid sequence '+ ex.args[0]) ``` --- Si otro error surgiera en nuestro código y ValueError lo capta, nuestro mensaje sería muy confuso, con el ejemplo anterior. Por eso, a veces se requiere crear nuestros propias excepciones usando la clase Exception. Te dejo el ejemplo, pero ntp, eso lo veremos más adelante cuando veamos clases. ```python class AmbiguousBaseError(Exception): pass def get_at_content(dna): if dna.count("N") > 0: raise AmbiguousBaseError(f'Sequence contains {dna.count("N")} N\'s') length = len(dna) a_count = dna.count('A') t_count = dna.count('T') at_content = (a_count + t_count) / length return at_content sequences = ['ACGTACGTGAC', 'ACTGCTNAACT', 'ATGGCGCTAGC'] for seq in sequences: try: print('AT content for ' + seq + ' is ' + str(get_at_content(seq))) except AmbiguousBaseError as ex: print('skipping invalid sequence '+ ex.args[0]) ``` --- ## Tarea Modifica el programa `count_atgc.py` y aplica la detección de los siguientes posibles errores o bugs que pueden romper/detener la ejecución del programa: Para el archivo de secuencia de DNA: - El programa debe mandar un mensaje "sorry, couldn't find the file", cuando el archivo no exista. [try/except] - El programa debe mandar el mensaje "sorry, the file is empty", cuando el archivo venga vacio. - El programa acepta letras ATGC mayúsculas o minúsculas. [try/except, raise, if, u otra solución] - El programa no acepta ningun otro caracter que no sean letras ATGC. Si existieran debera marcar "Sequence contains [caracter], it is invalid character". Para el argumento nucleotides: - Solo debe aceptar letras ATGC mayúsculas o minúsculas.