29 de diciembre de 2016

Errare humanum est


Durante el semestre académico que recién terminó (agosto-diciembre de 2016) tuve la oportunidad de dar el curso de Fundamentos de programación. Esta materia la he impartido pocas veces, sin embargo los aprendizajes que me llevo como educador siempre son muy valiosos. Es muy interesante trabajar con alumnos que recién comienzan su carrera profesional y que también están iniciando su inmersión al mundo de la programación de computadoras.

Grupo 4 de la materia
Fundamentos de programación del ITESM CEM,
semestre agosto-diciembre de 2016.

Cuando se comienza a aprender algo nuevo es de esperarse que se cometerán errores. Como dijo alguna vez el renombrado escritor irlandés Bram Stoker:
Aprendemos de los fracasos, no de los éxitos.
Errar es humano, y en esta entrada del blog de EduPython comentaré, a partir de mi experiencia, sobre los cuatro errores más frecuentes que cometen los alumnos cuando están aprendiendo a programar usando el lenguaje Python, así como algunas recomendaciones de cómo evitarlos.

Paréntesis mal balanceados

Este es el error más común que he observado al inicio del curso. Se tiene una instrucción la cual requiere del uso de paréntesis anidados, por ejemplo:
print(sqrt(x ** 2 + y ** 2)
En el código anterior se puede observar que está faltando cerrar un paréntesis al final. Al intentar ejecutarlo, el intérprete de Python marca usualmente un error de sintaxis. La cuestión es que dicho error se marca en la siguiente línea, no en la línea donde está realmente el problema.

Aquí recomiendo dos cosas:
  1. Acostumbrar a los alumnos a buscar los errores de sintaxis no solo en la línea donde se señala el problema, sino también una o varias líneas antes.
  2. Enseñar a los alumnos a comprender y utilizar las pistas visuales que brindan los editores al momento de cerrar paréntesis, llaves y corchetes. En el caso del IDLE, cuando se cierra alguno de estos elementos (paréntesis, llaves y corchetes) se ensombrece toda la expresión desde el elemento correspondiente de apertura. En la siguiente imagen puede uno notar que falta cerrar el paréntesis de la función print():

    IDLE

    Otros editores, como Spyder, parpadean o usan un color de fondo diferente para destacar los paréntesis que se están abriendo y/o cerrando: 

  3. Spyder

Indentación incorrecta

En Python la indentación sirve para indicar que un grupo de instrucciones conforman un mismo bloque. Este efecto se logra con llaves ({ y }) o las palabras reservadas begin y end en otros lenguajes.

La indentación obligatoria de Python es algo positivo, ya que obliga a los alumnos a adquirir prácticas de escritura de código legible. Normalmente los editores para código de Python ayudan a producir programas con la indentación correcta sin requerir mucho esfuerzo. Sin embargo se pueden producir errores cuando se copia y pega código de otro lado o cuando se insertan o borran espacios de manera no intencional, por ejemplo:

Programa con error de indentación

En el programa de arriba, la línea 5 está indentada con un espacio más hacia la derecha con respecto a las otras líneas del mismo bloque (las líneas 3 y 8). El editor, Spyder en este caso, nos echa la mano al señalar que hay un problema en el programa colocando un signo de admiración en el margen izquierdo en la línea infractora.

Mis recomendaciones para evitar este error:
  1. Enfatizar a los alumnos sobre la importancia que tiene la indentación y la correcta alineación de las instrucciones para establecer la estructura lógica de sus programas. Si van a copiar y pegar código, se deben asegurar que todas las instrucciones queden alineadas en el lugar debido.
  2. Nunca mezclar en un mismo programa caracteres de espacio (código ASCII 32) con caracteres de tabulador (código ASCII 9). Esto no debe ser problema en la mayoría de los editores que soportan Python. Cuando el usuario presiona la tecla «Tab», usualmente los editores insertan cuatro espacios en lugar de insertar el carácter de tabulador. Sin embargo este comportamiento se puede cambiar en la configuración de la mayoría de los editores. Yo recomiendo no alterar la configuración en este sentido ya que no es fácil distinguir a primera vista un tabulador de una secuencia de varios espacios.

Uso incorrecto de return

Las funciones son uno de los mecanismos de abstracción más poderosos que tenemos disponibles en Python y en casi cualquier otro lenguaje de programación. Yo generalmente cubro este tema desde muy temprano en el curso de Fundamentos de programación. Cuando los alumnos comienzan a escribir sus propias funciones el principal error que observo es el uso incorrecto de la instrucción return. Los errores caen en alguno de los siguientes dos casos:
  • return inexistente. Esto sucede cuando una función implementa correctamente un cierto algoritmo, pero carece de la instrucción return necesaria para devolver el resultado correspondiente. Por ejemplo, la siguiente función cuenta la cantidad de números negativos contenidos en una lista:
    def cuenta_negativos(a):
        resultado = 0
        for x in a:
            if x < 0:
                resultado += 1
    
    En Python todas las funciones siempre devuelven un valor. Por omisión devuelven None, a menos que explícitamente se indique otro valor dentro de una instrucción return. En el ejemplo anterior no hay un return explícito, así que la función siempre devuelve None. Por tanto, la forma correcta de escribir cuenta_negativos es:
    def cuenta_negativos(a):
        resultado = 0
        for x in a:
            if x < 0:
                resultado += 1
        return resultado
    
    Una variación de este problema es cuando una función tiene varias condiciones que utilizan instrucciones return pero no son exhaustivas. Por ejemplo, analicemos la siguiente función:
    def positivo_o_negativo(x):
        if x < 0:
            return 'negativo'
        if x > 0:
            return 'positivo'
    
    Esta función devuelve las cadenas 'negativo' o 'positivo' si el parámetro x es menor o mayor a cero, respectivamente. Sin embargo, ¿qué pasa si x es igual a cero? Tenemos aquí un código en donde las condiciones no son exhaustivas produciendo un bug muy sutil. Al no cumplirse alguna de las dos condiciones la función termina sin ejecutar un return explícito, por tanto devuelve None. Hay varias formas de corregir el problema. Una de ellas consiste en suponer que si un número no es negativo debe ser entonces positivo. Agregando una cláusula else al if tenemos:
    def positivo_o_negativo(x):
        if x < 0:
            return 'negativo'
        else:
            return 'positivo'
    
    Con este cambio la función jamás devolverá None siempre que x sea un valor numérico.
  • return fuera de lugar. En este caso el error consiste en incluir la instrucción return dentro de la función pero en un lugar que hace que el flujo de ejecución termine de manera prematura. Continuando con el ejemplo de la función cuenta_negativos, a menudo veo que algunos alumnos escriben erróneamente el código así:
    def cuenta_negativos(a):
        resultado = 0
        for x in a:
            if x < 0:
                resultado += 1
                return resultado
    
    La instrucción return se ejecutará cuando la condición del if resulte verdadera. El return provoca que la función termine inmediatamente, por lo que las iteraciones pendientes del ciclo for ya no se ejecutarán. En otras palabras, la función devuelve el valor 1 en cuanto encuentra la primer número negativo en la lista a. Si a no tiene números negativos el for concluye de manera normal, y como no hay posteriormente un return explícito entonces devuelve None. Una variación, igualmente incorrecta, es ésta:
    def cuenta_negativos(a):
        resultado = 0
        for x in a:
            if x < 0:
                resultado += 1
            return resultado
    
    Aquí el return es parte del bloque del for. Esto quiere decir que la función siempre termina en la primera iteración del ciclo, devolviendo uno si el primer número de a es negativo, o cero en caso contrario. Como caso especial, si la lista a está vacía devuelve None, ya que el ciclo for termina de manera normal y después no hay return explícito. Podemos observar que la única diferencia sintáctica entre la versión correcta y los dos códigos erróneos es el nivel de indentación que tiene la instrucción return.
Para evitar errores asociados al uso incorrecto del return recomiendo:
  1. Resaltarle a los alumnos la manera en que trabaja la instrucción return. Es muy importante que entiendan que cuando se ejecuta esta instrucción la función termina de manera inmediata sin importar que estuviera en medio de un ciclo o que existieran otras instrucciones posteriores pendientes a ser ejecutadas. Si una función está devolviendo None seguramente está haciendo falta un return explícito en algún lado.
  2. Promover que los alumnos realicen pruebas de escritorio de sus funciones, utilizando diferentes valores de entrada y procurando revisar de manera exhaustiva los casos normales y extremos.

Variables fuera de alcance

Para definir una variable en Python basta con asignarle un valor inicial. Si esta asignación ocurre dentro de una función, la variable en cuestión es local a dicha función y eso significa que no se puede acceder a ella desde otras funciones. Las variables locales son algo bueno ya que tienen un alcance y tiempo de vida limitado a una región usualmente reducida de código. Esto conlleva a que una función sea, en principio, más fácil de entender y modificar de manera aislada. Sin embargo, es bastante común que algunos principiantes intenten acceder a variables que están fuera de alcance (definidas en otra función),  por ejemplo:
from math import sqrt

def hipotenusa():
    return sqrt(a ** 2 + b ** 2)

def principal():
    a = float(input('Primer cateto: '))
    b = float(input('Segundo cateto: '))
    c = hipotenusa()
    print('La hipotenusa es', c)
La función principal() define tres variables locales: a, b y c. La función hipotenusa() intenta usar dos de esas variables (a y b) produciendo un error ya que no están visibles en ese contexto. La forma más sencilla de corregir este problema es enviar como parámetros aquellos datos que deseemos compartir:
from math import sqrt

def hipotenusa(a, b):
    return sqrt(a ** 2 + b ** 2)

def principal():
    a = float(input('Primer cateto: '))
    b = float(input('Segundo cateto: '))
    c = hipotenusa(a, b)
    print('La hipotenusa es', c)
Los parámetros a y b de la función hipotenusa() reciben una copia de los valores de las variables locales a y b de la función principal(). Aquí utilizamos los mismos nombres (a y b) en ambas funciones por mera conveniencia, pero no tiene que ser así.

Mi recomendación para evitar problemas con variables fuera de alcance consiste en hacerles entender a los estudiantes cuáles son las implicaciones de que una variable sea local a una función. Así mismo, es necesario explicarles que cuando un programa requiere compartir información entre dos funciones se pueden utilizar parámetros para enviar datos de entrada y la instrucción return para devolver datos de salida. Esta es generalmente la manera más adecuada de diseñar un programa pues lo hace más legible y facilita su mantenimiento. Alternativamente, es posible usar variables globales o incluso definir funciones más extensas (por ejemplo unir hipotenusa() y principal() en una sola función). Sin embargo estas opciones provocan normalmente que el código sea más complicado de entender y modificar, por lo que conviene disuadir a los estudiantes de hacer esto, especialmente en las primeras etapas de un curso introductorio de programación.

Conclusión

En mi experiencia, los problemas más usuales que tienen los estudiantes al comenzar a programar en Python se pueden resumir en:
  • Falta de comprensión clara sobre algunos conceptos fundamentales (indentación, instrucción return, alcance de las variables).
  • Falta de conocimiento de cómo usar las herramientas disponibles (principalmente el editor).
Los alumnos aprenden a evitar caer en ciertas trampas al momento en que entienden bien los conceptos y obtienen más experiencia usando las herramientas apropiadas.

Amigo lector, si eres maestro y enseñas a programar, ¿qué tipo de errores has visto que tus alumnos cometen frecuentemente? ¿Cómo le haces para evitar que los cometan? Usa la sección de comentarios para compartir tus experiencias. Gracias de antemano.