Python para Ciencias de la Tierra

Capítulo 3: Mejorando tu Programa (Pt. 1)

Damian
18 min readApr 7, 2022

--

Índice del curso
<<
Capítulo 2: Conceptos Básicos de Programación (Pt. 6)

3.1 Depuración del Programa

Nota antes de comenzar: Puedes descargar el cuaderno de esta sesión, los archivos usados en los ejemplos y los ejercicios resueltos suscribiéndote a mi patreon: https://www.patreon.com/ciencias_tierra.

Hasta este punto te has encontrado con algunos errores en la realización de tus programas. Todos los cometemos, por muy experimentados que seamos, siempre hay una letra o un pequeño comando que se nos escapa u omitimos. Ahora vamos a dar un repaso de los tipos de errores generales que te puedes encontrar, porque a partir de ahora los cometerás más a menudo. Pero descuida que no es porque no sepas, sino porque los programas conforme crecen, se complican, y con ello, los errores aumentan. Pero veremos recomendaciones para evitarlos. Al proceso de buscar y solucionar estos errores; y mejorar el rendimiento del programa se conoce como depuración.

Tipos de Errores

Existen diversos errores que puedes cometer, los cuales pueden agruparse en tres clases distintas: de sintaxis, lógicos y semánticos.

Errores de sintaxis (Syntax errors)
Estos son los errores más comunes que seguro ya has cometido. Python te muestra estos errores porque significa que has violado sus reglas “gramaticales”, es decir, no has escrito bien alguna función, algún método, alguna coma, etcétera. Es un tipo de error tan cotidiano que equivale al signo negativo que olvidabas poner en tus respuestas aritméticas o algebraicas mientras cursabas tu educación matemática básica.

Python hace todo lo que puede para señalar el lugar exacto donde está el error, sea una línea o un carácter donde ha detectado el fallo. Pero es común que el error que debe corregirse esté en realidad en una línea anterior o en una posición diferente. Aunque puede tomar incluso minutos en encontrarse el error, este tipo es el más fácil de solucionar, pues sólo bastará con escribir o cambiar un carácter, o en el mayor de los casos reescribir una pequeña porción de una línea de código para su solución.

Errores lógicos
Estos errores ocurren cuando un programa tiene una sintaxis correcta, es decir, está bien escrito, pero existe un error en el orden de las sentencias o en la forma en que están relacionadas unas con otras. Un ejemplo muy común es querer usar una variable definida después de que se usa por primera vez. Para estos errores Python identifica por dónde registró el error, pero no dónde está. Por lo que resulta más complicado identificarlo, porque hay que encontrar dónde está el punto en el que la variable o la sentencia inició el problema.

Errores semánticos
Has superado los dos tipos de errores previos. Tu programa está sintácticamente perfecto y con las sentencias en el orden lógico correcto, pero sencillamente hay un error en el programa. Python no te dice que hay un error, pero sabes que lo hay porque no hace lo que tú pretendías que hiciera. El problema puede ser desde una sentencia lógica mal planteada, como indicar un mayor o igual que donde debería haber un diferente que; hasta un mal entendimiento de la cuestión que se quiere resolver.

Son los errores más difíciles de solucionar porque el problema no es con el lenguaje Python, el cual ya dominas bien, sino con la manera en que formulaste el algoritmo. Su solución consiste en que revises el algoritmo más allá del código; mediante un diagrama de flujo u otros diagramas o esquemas que faciliten el entendimiento del problema.

Es posible que el programa sólo haga una porción correcta del problema. Al verlo, te verás tentado a pensar que es una buena idea añadir más líneas de código para ‘ajustarlo’ a lo que le falta hacer. Error. Saturar tu código de líneas y líneas para corregir este tipo de problemas no hacen más que complicar su estructura, haciéndolo menos entendible para ti o para otras personas; o puede hacer menos eficiente el programa. En resumen, puede causar más problemas de los que quieres solucionar. Recuerda el zen de python: Simple es mejor que complejo, y complejo es mejor que complicado. Si el código se torna muy complicado incluso de explicar, es mejor que pienses en otro algoritmo. Sí, eso implica aparentemente retroceder varios pasos. Y como principiante, estoy seguro de que eso puede ser muy frustrante para ti, pero habrá ocasiones que tendrás que regresar a replantear cómo resolver un problema.

Figura 3.1 Diagrama que muestra la dificultad para resolver un error según el tipo de error.

Depuración

Cuando tu programa presenta algún tipo de los errores descritos arriba, empieza una intensa búsqueda por la causa del error. Es momento de depurar. Cuando depuras un programa existen cuatro cosas por hacer:

  • Leer. Así es, tienes que leer cuidadosamente tu código. Revísalo y léelo de de nuevo, para asegurarte de que ahí está expresado de forma coherente lo que quieres decir. Considera que de ser un error tipográfico, puede que leer el código sea suficiente. Pero si es conceptual no será suficiente, pues debes comprender muy bien lo que hace tu programa contra lo que quieres que haga. Con ello te darás cuenta de que el error está en tu cabeza, y no en el código en sí.
  • Ejecutar. Puedes copiar una parte del código donde sospechas está el error, y ejecútala tantas veces como modificaciones le hagas. Haz pequeñas pruebas con datos que conozcas muy bien qué resultados deben dar, y luego prueba con los datos originales. Verifica si alguna función o método de algún objeto en verdad hace lo que crees que hace. Sólo ten cuidado de no caer en una mala práctica: ejecutar aleatoriamente cambios hasta que el programa haga lo que debería. Esto además de llevar mucho tiempo, te impedirá comprender bien tu programa, por lo que es más probable caer de nuevo en un error similar.
  • Pensar detenidamente. Tómate tu tiempo para pensar. Debes estar consciente que tal vez 5 minutos no basten. Identifica el tipo de error que tienes. Si Python lo identificó, piensa en el mensaje que te muestra; si no, piensa en la salida de datos que estás obteniendo, y plantea una o varias hipótesis de lo que lo causa. Piensa en el problema que estás solucionando. Toma un lápiz y papel, y haz dibujos, diagramas, apóyate de cualquier ayuda visual. Piensa en el o los últimos pasos que hiciste antes que se presentara el problema.
  • Retroceder. Puede suceder que tras leer cuidadosamente, hacer varias pruebas y pensar detenidamente, deberás tomar la difícil decisión de dar marcha atrás. Tendrás que deshacer los cambios recientes hasta obtener de nuevo un programa que funcione y puedas entender. Llegado a ese punto, podrás continuar con tu trabajo. Pero ten por seguro que todo el trabajo que hiciste al buscar el error te habrá dado un nuevo panorama para abordar el problema.

Pero también tómate un descanso. Debes hacer otra actividad, despejar tu mente. Y antes de decidir regresar a afrontar el problema, platícale a alguien o a ti mismo el planteamiento de la solución que propones. Esto te permitirá entender mejor tu programa.

Por otro lado, si hay demasiados errores, y además el código es demasiado extenso y complicado, por muchas técnicas de depuración que implementes difícilmente lograrás corregirlo por completo. Por lo que la última opción sigue siendo retroceder, y empezar de nuevo desde cierto punto.

Saturación de Memoria

La memoria es un recurso limitado en tu computadora. La ejecución de cada línea, las impresiones en pantalla, las variables que generas, todo consume memoria. Puede ocurrir que Jupyter se sature, y con ello sean más lentas las ejecuciones o peor aún, deje de funcionar en un proceso importante.

Las impresiones en pantalla se pueden moderar. Basta con estar consciente que no es necesario imprimir cada paso que realices para corroborar que vas por buen camino. Sólo hay que limitarse a mantener las impresiones de resultados relevantes. Con el tiempo limitarás tus impresiones, pues según adquieras más experiencia, tendrás mayor confianza en que lo que hiciste es correcto.

Por otro lado, cuando un programa crece, es probable que eventualmente se llene de variables que más adelante no sean necesarios en el mismo programa. Bien pueden ser variables relevantes al inicio del mismo, o variables que definiste de forma temporal para realizar pruebas. Ambos tipos pueden saturar la memoria. Para ello es importante saber en cierto punto qué variables hay y cuál es el espacio que ocupan, para hacer una depuración.

En Python, particularmente los IDE interactivos como Jupyter, existen comandos especiales llamados comandos mágicos. Estos comandos tienen la sintaxis %comando, y han sido diseñadas para agilizar la programación de código relacionado con el análisis de datos. No están asociadas a un objeto como los métodos, ni se pueden implementar en objetos como las funciones; sólo son comandos que para nuestros fines inmediatos muestran información relevante sobre el estado actual del programa.

Uno de esos comandos mágicos es %who y sus variantes. ¿Qué es %who? Como su nombre lo indica, nos permite saber quién o quiénes están en nuestro programa. Es decir, muestra las variables que están cargadas hasta el momento de su ejecución.

Este comando mágico puede carecer de importancia en otros IDE’s como Spyder, cuya interfaz ya incluye un visor de las variables que ha generado el código. Pero en Jupyter es un instrumento útil para conocer mejor lo que has hecho en el código. Por ejemplo, podemos hacer un recuento de todas las variables que se generaron en el cuaderno de la sesión anterior.

Figura 3.2 comparativa del uso de los comandos mágicos %who (celda superior), %who_ls (celda inferior izquierda) y %whos (celda inferior derecha). El comando %whos es el que muestra más información de todas las variables, en cambio %who_ls muestra todas las variables en forma de lista, lo cual permite manipularlas.

Ahora que sabemos qué variables hay, podemos identificar cuáles ya no son utilizadas. También podemos conocer la memoria que ocupa cada variable en bytes gracias a la función getsizeof() del módulo sys, el cual es propio de la librería estándar de python.

import sysprint(sys.getsizeof(5), sys.getsizeof('5'), sys.getsizeof(5.))

>> 28 50 24

Y vemos que la cadena es el valor que ocupa más espacio en la memoria, básicamente el doble que sus contrapartes numéricas. El valor entero 5 ocupa 28 bytes y el flotante 24, siendo el valor de menos espacio ocupado.

Esta función en conjunto con el comando mágico %who nos permite conocer mejor a nuestras variables, para decidir qué podríamos borrar, así como entender mejor cómo estamos desarrollando nuestro código.

Figura 3.3 Ejemplo de código hecho para saber cuál es la memoria que ocupa cada variable en nuestro programa, y la memoria total de las variables.

En la imagen previa se muestra el espacio que ocupan las variables. Si bien es poco espacio, pues la mayoría de las variables están formadas por un valor, este concepto es importante porque más adelante trabajarás con objetos más grandes, y harás estructuras de datos con mayor número de elementos.

El comando del elimina por completo una variable de nuestro programa.

#Definición de variables a manera de ejemplo
a, b, c = [1, 2, 3], 'cadena', 3.1416
print(a, b, c)

>> [1, 2, 3] cadena 3.1416

del bprint(a, b, c)

Y si ejecutas esta última celda, te mostrará error, indicando que b no está definida. Ahora el programa sólo tiene a las variables a y b.

3.2 Interacción con el Usuario

Hasta ahora los programas que has hecho se centran en la realización de una tarea, es decir, tu programa hace de forma automática las tareas que se le asignan. Desde el inicio de este curso se habló que se trataría muy poco la experiencia de usuario puesto que no es el objetivo. Pero es necesario agregar dinamismo a nuestros programas como si un usuario externo fuera a interactuar con ellos.

En general harás un programa pensando en que tú eres ese usuario externo. Piensa que tal vez desees realizar alguna tarea a medio proceso que le permita al programa continuar con ciertas acciones basadas en tus decisiones. O hacer un programa que procese datos de un archivo externo cuantas veces lo desees, tendrás que agregar el nombre o la ruta de acceso a tu programa. Las razones pueden ser varias.

Entrada de datos

La función que permite la admisión de datos de un usuario es input(). Similar a la función print(), se debe indicar dentro de los paréntesis la cadena que indica la instrucción para ingresar datos. Esta cadena se debe almacenar en una variable para decidir qué hacer con los datos de entrada o qué respuesta dar, de acuerdo con lo esperado en el programa.

En la imagen inferior se muestra cómo se ve un programa a la espera de un dato de entrada, donde el asterisco indica que está corriendo el programa, y no dejará de correr hasta que se presione la tecla enter.

Figura 3.4 De la celda superior a la inferior: el programa queda a la espera de ingresar un dato, se introduce el dato y se termina de ejecutar al presionar la tecla enter.

La función input() siempre considera una cadena como valor de entrada, por lo que de esperar un número, el usuario lo puede teclear, pero el programa debe convertirlo en el tipo numérico que se desee, de acuerdo a los métodos de conversión vistos en el capítulo pasado. Por ejemplo, si deseas capturar datos de valor entero de muestras obtenidas en campo, tu programa te puede preguntar hasta qué punto se debe terminar la captura. El dato ingresa como cadena, pero el programa lo convierte en entero.

Figura 3.5 Ejemplo de un programa que realiza el mismo proceso cuantas veces lo indique el usuario.

Al ser tú ese usuario externo, en principio no debe haber necesidad de dar demasiadas especificaciones para entender cómo se introduce el dato. Pero hay errores involuntarios que nos hacen introducir tal vez una letra que no pueda convertirse en número. Para ello más adelante veremos cómo evitar estos problemas mediante sentencias de control.

Salida de datos

Ya hemos visto casos donde los datos pueden exportarse a un archivo externo, sea un archivo de texto plano o un archivo delimitado por comas. Lo importante es que el valor exportado tenga el tipo de dato adecuado para su fácil interpretación por algún programa informático. Por otro lado, un resultado también puede ser mostrado en pantalla o ser parte de un texto en el que deba ser claro lo que se dice. En este caso es posible que se desee darle un formato para más allá de la estética, agilizar su interpretación. En esta sección veremos un comando muy importante llamado format(), la cual ya utilizaste brevemente en el ejemplo 2.4.

Hay dos maneras de utilizar el format para dar estilo a una cadena. Una es como método de una cadena, dentro de la cual se indica el formato encerrándolo en llaves, y como parámetro la o las variables que deben estar en esa cadena.

r = 6371
g = 9.80665
print('La Tierra tiene una aceleración gravitacional media de ' '{:.2f} m/s^2 y un radio medio de {:,} km.'.format(g, r))

>> La Tierra tiene una aceleración gravitacional media de 9.81 m/s^ 2 y
>> un radio medio de 6,371 km

Las llaves {:} se ubican donde deseamos tener el valor en la impresión, y las variables se agregan como parámetro en el orden en que se han agregado las llaves. Los dos puntos siempre deben escribirse. En las primeras llaves la f se refiere a que se da estilo a un valor flotante, y en las segundas llaves la coma se refiere a que separe la cantidad cada tres cifras.

Figura 3.6 Diagrama que muestra la sintaxis usada para dar formato a números flotantes y enteros.

Al agregar la cantidad de lugares que debe ocupar un número sin importar sus cifras, en automático los números se alinean a la derecha, dejando los espacios en blanco por falta de cifras. Observa como ejemplo los dos códigos de la imagen, donde uno tiene formato y el otro no.

Figura 3.7 En el código superior los números enteros tienen habilitado un número de espacios fijo en su formato. Observa cómo se alinean a la derecha para cubrir esa cantidad de espacios. En el código inferior se muestran números enteros sin formato; siempre están alineados a la izquierda.

Este formato operaría de la misma forma con un valor flotante, donde además el punto y la parte decimal también ocupan espacios.

Nota: En Python 2.x en lugar del método format, se utilizaba el operador % al término de la cadena y previo a los paréntesis.

La otra forma de utilizar el format es como una función, donde el primer parámetro es la variable a formatear, y el segundo parámetro es el estilo a darle. La sintaxis de formato carece de las llaves y de los dos puntos, y sólo basta indicarla entre comillas. El resto de la sintaxis es la misma. Esta función sólo admite dar formato a una variable a la vez.

print(format(g, '.2f'))

>> 9.81

¿Cuándo usar una u otra forma de dar formato? Como método, format permite asignar un formato a varias variables en una cadena, la cual puede ser para su impresión como para su guardado en un archivo de texto. Por otro lado, como función, format sólo admite una variable a la vez, pero mediante un ciclo pueden guardarse todos los valores en un mismo formato, por ejemplo.

En esta sección nos hemos centrado en el estilo de números, puesto que representan el tipo de dato que más se trabaja en una carrera con enfoque científico. Pero también se puede dar formato al texto. Como muestra está el código de la siguiente imagen.

Figura 3.8 Ejemplo del formato que se puede dar al texto para su impresión.

Al final de la sesión te comparto enlaces donde puedes aprender con más detalle cómo dar formato a diversos tipos de variables.

3.3 Sentencias de control

Las sentencias de control son un conjunto de comandos que permiten optimizar el código pues controlan uno o varios pasos de éste. En la mayoría de los casos evitan que se ejecuten líneas que en ciertos pasos se vuelven innecesarias. También facilitan la búsqueda de errores.

Continue, Break, Pass

Continue
Esta sentencia permite terminar un ciclo, y lo salta al siguiente cuando una condición se cumple. Es muy útil si quieres que bajo ciertas condiciones un ciclo no se termine y se salte al siguiente, evitando ejecutar el resto de las líneas. Observa el diagrama a continuación.

Figura 3.9 Diagrama de flujo de la sentencia continue dentro de un ciclo for.

Como ejemplo, podemos ejecutar el siguiente código.

for n in range(10):
if n%3 == 0 or n%2 == 0:
continue
print(n)

>> 1
>> 5
>> 7

Sólo imprime esos números porque se le indicó que salte al siguiente ciclo en caso de que algún número sea múltiplo de 3 o de 2.

Break
Esta sentencia termina el ciclo, es decir, se sale del ciclo actual y no ejecuta ningún otro ciclo que falte, de acuerdo con una condición dada.

Figura 3.10 Diagrama de flujo de un ciclo for que en caso de cumplir una condición se ejecuta la sentencia break, saliendo por completo del ciclo.
for n in range(10):
if n == 7:
break
print(n)

La impresión de este código serán los números del 0 al 6, porque en el 7 de acuerdo con la condición, se sale del ciclo.

Pass
Es una sentencia que no hace nada, que el intérprete al leerla sigue ejecutando el código como si no hubiera nada en esa línea, como si fuera una línea en blanco. Puede utilizarse en cualquier bloque del programa como ciclos o funciones, al cabo no afecta en nada al código, en serio. Y es ahí donde radica su utilidad.

for i in range(5):
pass

Si ejecuta este código notarás que nada sucedió, la sentencia pass no afectó al programa. Y eso está bien ¿Por qué? Porque pass es utilizado como marcador de posición. Cuando desarrollas un programa, no siempre tienes todas las ideas claras, pero puede que sepas dónde usarás un ciclo, por lo que pass te deja marcar que hay un ciclo pendiente por hacer. Si quitas la sentencia, generará error pues no se puede tener un bloque vacío. Entonces, una forma más adecuada de escribir el ciclo for anterior sería:

for i in range(5):
#Por hacer: imprimir el doble del valor i
pass

Se escribe un comentario como nota recordatoria de lo que se debe hacer, mientras desarrollas el resto del código con la seguridad que tener un bloque ‘vacío’ no causará errores.

Así que pass sí se ejecuta, pero no resulta en nada.

Try - Except

Hemos visto que podemos recibir diversos datos de entrada de fuentes como un archivo de texto plano, un archivo delimitado por comas, o introducido por un usuario. En general se espera que ingrese como cadena y de acuerdo con lo requerido se convierta a otro tipo de dato. ¿Pero qué sucede si se introduce una letra a un programa que espera un número? Esto provocará un error en nuestro programa.

Como programadores, debemos preparar a nuestros programas para recibir datos no esperados. La sentencia try - except nos permite combatir estos errores. Literalmente hace que el programa intente hacer algo, que en caso de no ser lo que se espera, ejecute lo que se indique en la excepción. En la siguiente imagen, de no ser por esta sentencia, el error provocaría una ruptura en todo el programa, saliendo del ciclo. Ahora sólo son unos pocos datos que se perderían de no tener el try - except, ¡pero imagina que eso suceda en el dato 97,500 de 100,000!

Figura 3.11 Ejemplo de una estructura try - except. Se envía un mensaje en caso de cometerse un error, pero no se sale del ciclo, perdiendo todos los datos ingresados.

Esta pareja de sentencias siempre debe estar en la misma estructura. No puede haber un try sin un except, y viceversa. Pero hay otras dos sentencias que opcionalmente pueden escribirse en esta estructura: else y finally.

La sentencia else se ejecuta si no se ejecuta la excepción, es decir, si no hubo error. De forma contraria, la sentencia finally se ejecuta obligatoriamente, sin importar que haya o no habido error. Ambas sentencias son opcionales.

Una estructura completa de las cuatro sentencias se vería como sigue:

try:
<Se ejecuta alguna tarea>
except:
<De haber error en la tarea, se ejecuta este bloque>
else:
<Se ejecuta este bloque sólo si se ejecuta el try>
finally:
<Siempre se ejecuta este bloque, haya o no error en la tarea>

3.4 El Fichero .py

Python como la mayoría de los lenguajes, permite la portabilidad de su código mediante el uso de un archivo de texto plano. Para python es el fichero .py. Este archivo guarda el código para su reproducción en otra computadora con otro sistema operativo y otra IDE o cualquier intérprete de Python.

Sin embargo, como se mencionó al inicio del curso, jupyter genera un archivo .ipynb, el cual no puede ser reproducido por otras IDE’s que no sean parte del proyecto Jupyter, ni por el intérprete de python. Por ello es recomendable tener un respaldo en fichero .py de algún proyecto importante.

Ejemplo 3.1 Hacer un fichero .py a partir del ejemplo 2.5.

El ejemplo 2.5 ha sido el más extenso que se ha hecho hasta ahora. En él se tuvo que hacer un programa que importara una tabla con mediciones de echados y espesores aparentes de una secuencia estratigráfica. Una vez importado, se agregó una nueva columna con los espesores rales a la tabla, para finalizar exportando esa tabla en un nuevo archivo.

Figura 3.12 Vista de todas las celdas que se utilizaron para la resolución del ejemplo 2.5

Para convertirlo en fichero, basta con copiar el código de cada una de las celdas, al tiempo que se pegan en una celda, respetando el orden en que fueron ejecutadas.

Se debe omitir la tercera celda porque en ella sólo se escribió el código que permitiría calcular el echado real, y fue reutilizado en la siguiente celda donde se guardaron los resultados en el archivo. No queremos que nuestro fichero tenga código duplicado ni ejecute líneas innecesarias. Por ello también se deben omitir los print, porque eran un apoyo visual para corroborar que lo que hacíamos era correcto. Nuestro fichero al ejecutarse sólo generará el archivo de salida, y la impresión de la suma total de los espesores.

Para no perder los pasos que diste al escribir el código, puedes escribir alguna marca a manera de comentario que te permita ver qué la distinción original de celdas, como se ve en la siguiente imagen. Tampoco olvides que se utilizó la paquetería math, por lo que se debe agregar un import math al inicio.

Figura 3.13 Vista de todo el código del ejemplo 2.5, adecuadamente seleccionado, puesto en una celda.

Prácticamente el código que está en esa celda ya es un fichero. Basta con copiarlo y pegarlo en el editor de texto plano de tu preferencia, y guardarlo como calculo_espesores.py en la misma ubicación donde está este cuaderno. Para que funcione, debes tener el archivo Ejemplo_2–5.csv que se utiliza para generar el nuevo archivo.

Y este fichero puede ser leído por otros IDE’s, como Spyder, que al ejecutarse habrá generado un archivo de salida, e impreso el texto resultado.

Figura 3.14 Vista del IDE Spyder tras ejecutar el archivo calculo_espesores.py

Fin del ejemplo 3.1

Puedes descargar el cuaderno de esta sesión, los archivos usados en los ejemplos y los ejercicios resueltos suscribiéndote a mi patreon: https://www.patreon.com/ciencias_tierra.

Ejercicios

3.1 - Sin utilizar una variable de cadena y ninguno de sus métodos, sólo valores enteros, imprime un número formado por una secuencia de valores 12345…n donde n es un valor entero ingresado por un usuario. ***

Ejemplos:
Sea n = 6, la impresión debe ser el número entero 123456
Sea n = 13, la impresión debe ser el número entero 12345678910111213

Indica que el entero debe ser entre 1 y 99, y de ser fraccionario, se truncará al valor entero. De ingresar un número fuera del rango, imprimir un mensaje explicando que debe ser dentro del rango indicado. De ingresar una cadena, imprimir un mensaje indicado el error que se está cometiendo.

--

--

Damian

Anything I want and is related to data. Learning to become a Data Professional.