22 de abril de 2017

Reconciliando Python 2 y 3

Python 3 fue liberado oficialmente hace más de ocho años. Sin embargo, todavía en el año 2017 existen razones que nos obligan a tener que escribir código para Python 2. Puede ser que necesitemos usar alguna biblioteca o programa preexistente escrito en Python 2. O quizás haya algún servicio en línea para aprender a programar que solo funcione con Python 2, por ejemplo PySchools.com. Tal vez requiramos usar algún servidor remoto de Linux que solamente tenga instalado Python 2, y carecemos de los privilegios de administrador necesarios para instalarle software nuevo. También ocurre que en algunos sitios de programación competitiva sus respectivos jueces en línea solo aceptan soluciones escritas en Python 2, como son los casos de omegaUp y el Caribbean Online Judge (COJ). Sea cual sea la razón, no deja de ser un tanto frustrante tener que estar lidiando con dos versiones del lenguaje.


Como educadores nos surgen varias dudas:
  • ¿Qué versión de Python debemos enseñar a alumnos que están aprendiendo a programar actualmente? 
  • ¿Debemos enseñar Python 2, viendo hacia atrás, privilegiando la compatibilidad con el pasado?
  • ¿Debemos enseñar Python 3, viendo hacia adelante, favoreciendo lo que pueden ser las ideas y prácticas del futuro? 
  • ¿Vale la pena, quizás, enseñar ambas versiones de Python? 
  • ¿Es posible escribir programas que funcionen sin modificaciones en las dos versiones? 
  • Pero para empezar, ¿por qué rayos existen dos versiones rivales de Python?

Un poco de historia

La primera versión pública del lenguaje (Python 0.9.0) apareció en 1991. Las versiones 1.0 y 2.0 fueron liberadas en 1994 y 2000, respectivamente. Para mediados de la década pasada, Guido van Rossum, el autor de Python, consideró que el lenguaje había acumulado, a través de los años, elementos redundantes que iban en contra de la filosofía esencial de Python, la cual establece que: “debe haber una, y de preferencia solamente una, manera obvia de hacer las cosas”. Así pues, Guido decidió que en la siguiente versión mayor de Python (originalmente llamada Python 3000 o Py3K) se eliminaran todos aquellos componentes considerados obsoletos o duplicados con el fin de obtener un lenguaje más limpio y elegante, con una mejor oportunidad de evolucionar positivamente sin tener que cargar con vestigios heredados del pasado. Obviamente, estos cambios provocarían una incompatibilidad con prácticamente todo el código de Python existente en aquel momento. Para facilitar la transición, se decidió que coexistieran durante algún tiempo dos versiones de Python. Así pues, a finales de 2008 teníamos disponibles las versiones 2.6 y 3.0 de Python. Así mismo, se proporcionó la herramienta 2to3 para simplificar la migración de los programas de Python a la nueva versión.

Guido van Rossum

En el año 2010 se liberó Python 2.7, la última versión menor de la rama de Python 2. Originalmente solo iba a ser soportada por cinco años. En otras palabras, se esperaba que para el 2015 todos hubiéramos ya migrado a Python 3. Lamentablemente esto no ocurrió y por eso la fecha de EOL (end of life o fin de vida) de Python 2.7 se extendió hasta el 2020. Esto significa que nos quedan por lo menos tres años en las que las versiones 2 y 3 de Python deberán seguir coexistiendo.

¿Qué hacer?

Yo soy de la opinión de que debemos concentrar nuestros recursos y esfuerzos en enseñar a nuestros alumnos Python 3.x, pues es la versión que tiene un futuro claro. Python 2.7 está actualmente en modo de mantenimiento y en pocos años estará descontinuado. No obstante, cuando veamos que surja la necesidad podemos orientar a nuestros alumnos a que utilicen algunas prácticas relativamente simples que permiten escribir programas que funcionan de manera idéntica tanto en Python 2.7 como en Python 3.x. En lo que resta de esta entrada del blog de EduPython explicaré cómo podemos lograr justamente esto.

Si no sabes qué versión del lenguaje estás utilizando puedes correr el siguiente código desde la consola de Python para averiguarlo:
>>> from sys import version
>>> version
'3.6.0 (default, Jan 13 2017, 00:00:00) \n[GCC 4.8.4]'
En mi experiencia hay cuatro diferencias fundamentales entre Python 2.7 y Python 3.x que pueden generarle algo de ruido a una persona que está aprendiendo a programar:
  • Caracteres especiales
  • Instrucción vs. función print
  • Funciones input y raw_input
  • Operador de división
Hay muchas otras situaciones en donde surgen incompatibilidades, pero con resolver las cuatro anteriores cubrimos las necesidades más comunes de un programador principiante. Para una descripción exhaustiva de cómo escribir código de Python compatible entre las versiones 2 y 3 se puede consultar la siguiente página: Cheat Sheet: Writing Python 2-3 compatible code.

Caracteres especiales

Por omisión, un archivo fuente de Python 2 utiliza el código ASCII como esquema de codificación de caracteres. Bajo este esquema solo podemos usar en nuestros programas las letras del abecedario inglés, los dígitos numéricos y algunos otros símbolos de puntuación. En otras palabras, no podemos usar ciertos símbolos propios del idioma español como son: las vocales con acento agudo u ortográfico (á, é, í, ó, ú, Á, É, Í, Ó, Ú), la letra u con diéresis (ü, Ü), la letra eñe (ñ, Ñ) ni los signos de apertura de interrogación y exclamación (¿, ¡). Si nuestro programa contiene cualquiera de estos caracteres no-ASCII, típicamente dentro de un comentario o en una cadena de caracteres, el intérprete de Python produce un mensaje de error parecido a éste:
SyntaxError: Non-ASCII character '\xc3' in file prueba.py on line 5, but no encoding declared;.
Para evitar este error podemos declarar de manera explícita el esquema de codificación de caracteres que utiliza nuestro archivo fuente de Python. Esto se hace incluyendo un comentario similar al siguiente en la PRIMERA o SEGUNDA línea del archivo (ver el documento PEP 263 para más detalles):
# coding=UTF-8
El formato de transformación de Unicode de 8 bits (UTF-8 por sus siglas en inglés) al que se hace referencia arriba es un esquema de codificación que soporta los caracteres de alfabetos modernos y extintos, sistemas ideográficos y colecciones de símbolos matemáticos, musicales, iconos, etc. Esto significa que podemos usar todos los símbolos del español y muchos más, incluyendo cosas como el signo de euro (€) o los palos de una baraja (♥, ♦, ♣, ♠).

Es importante mencionar que para que todo funcione correctamente el editor que se esté utilizando debe guardar el archivo usando el formato UTF-8 y no algún otro. Esto es algo totalmente transparente para el usuario si se está utilizando un editor diseñado expresamente para Python, como es el caso de IDLE o Spyder. Otros editores pudieran requerir alguna configuración explícita.


Python 3, a diferencia de Python 2, supone por omisión que los archivos fuente utilizan UTF-8 como esquema de codificación de caracteres, por lo que estrictamente es innecesario añadir el comentario “#coding=UTF-8” al inicio del archivo si deseamos usar dicho formato, sin embargo no le hace daño incluirlo y por compatibilidad conviene hacerlo. El documento Unicode HOWTO contiene muchos detalles sobre el uso de Unicode con Python.

Instrucción vs. función print

En Python 2 print es una instrucción mientras que en Python 3 es una función. La implicación más importante es que para invocar una función en Python se necesitan paréntesis pero no es así para una instrucción. Veamos unos ejemplos:
# Usando la instrucción print en Python 2
print 'Existen', 2 ** 10, 'bytes en un kibibyte.'
# Usando la función print en Python 3
print('Existen', 2 ** 10, 'bytes en un kibibyte.')
Al correr ambos códigos en sus respectivas versiones de Python la salida esperada es la misma:
Existen 1024 bytes en un kibibyte.
Sin embargo, si el código de arriba diseñado para Python 3 se corre en Python 2, los paréntesis se interpretan como los delimitadores de una tupla de tres elementos, produciendo una salida que resulta extraña cuando no se sabe lo que está sucediendo:
('Existen', 1024, 'bytes en un kibibyte.')
Por otro lado, si el código de arriba diseñado para Python 2 se intenta correr en Python 3, lo que se obtiene es un error de sintaxis:
  File "ejemplo.py", line 3
    print 'Existen', 2 ** 10, 'bytes en un kibibyte.'
          ^
SyntaxError: Missing parentheses in call to 'print'
Para resolver esta incompatibilidad debemos importar print_function del módulo __future__ al inicio de nuestro programa:
from __future__ import print_function
La instrucción anterior obliga a que print solo pueda usarse como función, independiente de la versión del lenguaje que se esté utilizando, por lo que el siguiente código:
from __future__ import print_function

# Usando print como función en Python 2 y 3
print('Existen', 2 ** 10, 'bytes en un kibibyte.')
produce exactamente la misma salida tanto en Python 2 como en Python 3:
Existen 1024 bytes en un kibibyte.


Al ser función, print tiene el beneficio adicional de que puede recibir varios argumentos de palabra clave:
  • sep: Indica qué cadena se debe utilizar como separador (o delimitador) entre los elementos a imprimir. Por omisión se usa un espacio en blanco (' '). 
  • end: Establece qué cadena se debe utilizar después de haber impreso todos los elementos. Se usa un salto de línea ('\n') en caso de no indicarse.
  • file: Define el archivo en el que se debe realizar la impresión. Si se omite usa la salida estándar (usualmente la pantalla).
El siguiente ejemplo muestra cómo usar todos los argumentos de palabra clave de la función print con la ventaja de que funciona de manera idéntica tanto en Python 2 como en Python 3:
from __future__ import print_function

with open('salida.txt', 'w') as f:
    print(4, 8, 15, sep='-', end='|', file=f)
    print(16, 23, 42, sep='*', end='$', file=f)
El código crea un archivo llamado salida.txt con el siguiente contenido:
4-8-15|16*23*42$

Funciones input y raw_input

Python 2 cuenta con dos funciones que permiten directamente leer datos desde la entrada estándar (usualmente el teclado):
  • raw_input: Facilita al usuario la captura de una línea de texto. Dicha línea es devuelta por la función como una cadena de caracteres pero sin incluir el carácter final de salto de línea ('\n').
  • input: Sirve para que el usuario ingrese una secuencia de caracteres que representan una expresión válida de Python. Dicha expresión es evaluada por el intérprete y el objeto resultante es el valor devuelto por la función.
Ambas funciones reciben un prompt (mensaje de entrada) como argumento opcional.


Python 3 solamente cuenta con la función input, que realmente es equivalente en términos de comportamiento al raw_input de Python 2. Si deseamos tener en Python 3 la misma funcionalidad que el input de Python 2 entonces tenemos que utilizar adicionalmente la función eval tal como se detalla en la siguiente tabla:

Función
de Python 2
Equivalente
en Python 3
x = raw_input('Texto: ') x = input('Texto: ')
x = input('Expresión: ') x = eval(input('Expresión: '))

NOTA: El uso de la función eval se considera un riesgo de seguridad pues permite al usuario inyectar código malicioso que pudiera comprometer la integridad del sistema. En general se recomienda evitar su uso.
Para que nuestro programa funcione igual en ambas versiones de Python debemos importar la función input del módulo builtins usando la siguiente instrucción:
from builtins import input
Después de esta instrucción la función input se comporta siempre como si estuviéramos en Python 3, aún estando en Python 2. No está de más mencionar que para obtener una completa compatibilidad entre versiones es importante que dejemos de usar la función raw_input ya que no está disponible en Python 3.
NOTA: Al parecer la instrucción “from builtins import input” produce un error en varias plataformas corriendo Python 2. El mensaje de error indica que no puede hallar el módulo builtins. Para corregir este problema se necesita ejecutar la siguiente instrucción con permisos de administrador desde una terminal de línea de comando:
pip2 install future
El siguiente código es un programa que usa input y que corre sin cambios en las versiones 2 y 3:
# coding=UTF-8

from __future__ import print_function
from builtins import input

num1 = int(input('Ingresa un número entero: '))
num2 = int(input('Ingresa otro número entero: '))
if num1 == num2:
    print('Ambos números son iguales')
elif num1 > num2:
    print(num1, 'es el número más grande')
else:
    print(num2, 'es el número más grande')
Conviene señalar que para convertir la cadena de caracteres devuelta por la función input a un número podemos utilizar las funciones int o float, dependiendo de si necesitamos un número entero o un número real (de punto flotante). Lo anterior es preferible a usar la función eval, la cual es innecesaria en este contexto y potencialmente peligrosa en la páctica.

Operador de división

El operador de división (/) funciona de manera diferente en las versiones 2 y 3 de Python.
NOTA: En Python el operador de división puede ser sobrecargado por cualquier clase (como es el caso de las clases numéricas complex y Fraction). Así pues, este operador puede tener un comportamiento muy diferente al aquí descrito cuando se usa con objetos que no sean específicamente números enteros o reales.
Veamos algunos ejemplos con Python 2:
# Python 2
>>> 20 / 3
6
>>> 20 / 3.0
6.666666666666667
Aquí podemos ver que al dividir dos números enteros el resultado da un número entero (el cociente de la división sin la parte fraccionaria). Por otro lado, se obtiene un número real cuando al menos uno de los operandos de la división es un número real. Esta forma particular de realizar la división es un legado del lenguaje C que generalmente resulta sorpresivo y poco intuitivo para la gente que está aprendiendo a programar.

La división en Python 3 funciona de manera más predecible:
# Python 3
>>> 20 / 3
6.666666666666667
>>> 20 / 3.0
6.666666666666667
En este caso el operador de división siempre produce un número real como resultado sin importar si sus operandos son enteros o reales. Este comportamiento resulta más accesible para los programadores principiantes.


Para poder usar el operador de división de forma que sea compatible con Python 2 y 3 debemos importar division del módulo __future__:
from __future__ import division
La instrucción anterior garantiza que el operador de división devolverá un resultado real. El siguiente programa produce los mismos resultados en las dos versiones de Python:
# coding=UTF-8

from __future__ import print_function
from __future__ import division

print(1 / 2)   # División verdadera (true division)
print(1 // 2)  # División de piso (floor division)
La salida de este programa es:
0.5
0
El ejemplo anterior muestra también el uso del operador de división de piso (//) el cual calcula el cociente entero de la división.

Resumen de consejos de compatibilidad

Aquí está el resumen de los cuatro consejos discutidos para escribir código fuente compatible con Python 2.7 y Python 3.x:
  • Si necesitas usar caracteres que no son parte del código ASCII (caracteres con tilde, eñe, etc.) en un archivo fuente de Python, entonces guárdalo usando el formato UTF-8 y añádele el siguiente comentario en la primera o segunda línea:
    # coding=UTF-8
    
  • Utiliza print siempre como función y no como instrucción. Añade el siguiente import a tu programa para que así sea:
    from __future__ import print_function
    
  • Para que tu programa lea una cadena de caracteres desde la entrada estándar (típicamente el teclado) utiliza únicamente la función input y solo después de usar el siguiente import:
    from builtins import input
    
    Utiliza las funciones int y float si requieres convertir la cadena de entrada a un valor numérico.
  • Para que el operador de división (/) siempre produzca un resultado real, sin importar si sus operandos son reales o enteros, añade a tu programa el siguiente import:
    from __future__ import division
    
    Utiliza el operador de división de piso (//) si requieres el cociente entero como resultado de dividir un número entre otro.

24 de marzo de 2017

Formación en competencias

Desde hace algunos años el Tecnológico de Monterrey, la institución en la que laboro, ha buscado enfatizar en sus programas de estudio universitarios el desarrollo de competencias. Pero, ¿qué es exactamente una competencia? Esta es la definición que el Tecnológico está actualmente manejando[1]:
Una competencia es la integración consciente de conocimientos, habilidades, actitudes y valores que permite enfrentar con éxito situaciones tanto estructuradas como de incertidumbre y que puede implicar procesos mentales de orden superior.
Alternativamente, se puede decir de manera más sencilla que una competencia consiste en “saber hacer algo con los conocimientos aprendidos”[2].


Las siguientes son algunas características de una educación basada en competencias[3]:
  • Es un enfoque alternativo al enfoque predominante actual, el cual está basado en objetivos.
  • En lugar de enfatizar conocimientos enfatiza el desarrollo de competencias.
  • El aprendizaje deja de ser relevante exclusivamente en un marco meramente hipotético, sino que su relevancia se extiende también al mundo real.
  • Busca formar al estudiante de manera integral dentro de su contexto personal y social, y no únicamente en el aspecto profesional.
  • Va más allá de la comprensión de conceptos, abarcando también el dominio de procedimientos y el desarrollo de actitudes.
  • Requiere de una instrucción intencional y bien planeada por parte de los maestros, con el fin de brindar la mejor experiencia de aprendizaje.
En resumen, la educación basada en competencias está orientada a la formación del alumno y no solo a la transmisión de conocimientos.


Es importante no confundir el concepto de competencia con el de objetivo de aprendizaje. Por un lado, un objetivo indica qué queremos que nuestros alumnos sepan. Por otro lado, las competencias establecen la forma en que podemos tener la certeza de que efectivamente lo saben. Las competencias definen los conocimientos y habilidades que un futuro egresado podrá desempeñar de manera exitosa en el mundo real, mientras que los objetivos de aprendizaje son específicos a la instrucción de un curso en particular[4].


Según Brooks, la educación basada en competencias ofrece múltiples ventajas para los estudiantes[5]. En principio cualquier alumno puede:
  • Trabajar a su propio ritmo y en el horario que más le convenga.
  • Construir nuevos aprendizajes a partir de sus conocimientos previos.
  • Graduarse de manera más rápida y por tanto ahorrar dinero.
  • Obtener experiencia práctica del mundo real.
Por otro lado, existen algunas posibles desventajas[6]:
  • Se enfoca más a las necesidades inmediatas de los empleadores y menos en la preparación de los estudiantes para que puedan ser flexibles en un futuro incierto.
  • No es adecuado para aquellas áreas en las que resulta difícil prescribir competencias específicas o donde se requieran incorporar nuevas habilidades o conocimientos de manera extremadamente rápida.
  • Hace que el aprendizaje tome un enfoque objetivista.
  • Esta manera de aprender pudiera resultarle inadecuada a algunas personas.
Es importante también comprender la manera en que se clasifican las competencias:
  • De acuerdo a su alcance: pueden ser transversales o disciplinares.
  • De acuerdo a su ámbito o dimensión: pueden ser personales, interpersonales, sociales o profesionales.
  • De acuerdo a su ubicación en el currículo: pueden ser básicas, intermedias o terminales.
Finalmente, vale la pena comentar sobre la forma de documentar las competencias. Se sugiere redactarlas utilizando la siguiente estructura[1]:

Estructura de una competencia

Cada elemento del diagrama anterior se describe a continuación:
  • Verbo: ¿Qué hace?
  • Objeto: ¿Qué transmite?
  • Contexto o condición: ¿En qué contexto o bajo qué condiciones?
Por ejemplo, para un curso de introducción a la programación podríamos tener la siguiente competencia:
Resuelve un problema de programación a partir de un enunciado textual el cual requiera emplear una instrucción condicional if-else.
Ahora nos queda a los profesores del Tec de Monterrey el reto de aplicar efectivamente estas ideas a nuestra práctica docente cotidiana. Aún queda mucho por aprender, pero no deja de ser emocionante la expectativa de poder brindar a nuestros alumnos cada vez mejores experiencias de aprendizaje.

Profesores del área de TI del Tecnológico de Monterrey.
Reunión nacional de grupos colegiados por disciplina,
Monterrey, Nuevo León. Julio de 2015.


Referencias

  1. Vicerrectoría Académica del ITESM, Dirección de Programas Académicos. Definición y desarrollo de competencias. México, febrero, 2017.
  2. Instituto Tecnológico de Estudios Superiores de Monterrey. Diseño y desarrollo de competencias. México, 2016.
  3. Instituto Tecnológico de Estudios Superiores de Monterrey. Educación basada en competencias. México, 2015.
  4. Vasquez, Caroline. Competencies and Learning Objectives. Estados Unidos, mayo, 2011.
  5. Brooks, Ashley. 6 Benefits of Competency-Based Education that will Excite Adult Learners. Estados Unidos, abril, 2016.
  6. Bates, Tony. The strengths and weaknesses of competency-based learning in a digital age. Canadá, septiembre, 2014.

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.