Mostrando las entradas con la etiqueta Programando en Python. Mostrar todas las entradas
Mostrando las entradas con la etiqueta Programando en Python. Mostrar todas las entradas

21 de diciembre de 2018

A las pruebas me remito

Siempre que escribimos un programa computacional necesitamos verificar que efectivamente funciona tal como se espera. Usualmente ejecutamos el código, si es necesario proporcionamos algunas entradas, y finalmente observamos si la salida obtenida es la que deseamos. De no ser así, hay que revisar el programa, hacer las correcciones necesarias, y repetir todo el proceso anterior. La acción de probar y verificar que un programa genera los resultados esperados usualmente se hace manualmente y no de manera automática. Esto puede resultar tedioso y consumir mucho tiempo cuando el software siendo escrito va más allá de una aplicación relativamente pequeña.

Fuente: www.flaticon.com

Afortunadamente, existen las pruebas unitarias automatizadas, las cuales son porciones de código diseñadas para comprobar que el código principal está funcionando de la manera esperada.

Veamos un ejemplo. Supongamos que deseamos escribir en Python una función que calcule la hipotenusa c de un triángulo rectángulo dados sus catetos a y b:


El teorema de Pitágoras establece que el cuadrado de la hipotenusa es igual a la suma de los cuadrados de las catetos respectivos, es decir: $$ c^2 = a^2 + b^2 $$ $$ c = \sqrt{a^2 + b^2} $$ En Python la función hipotenusa podría quedar codificada de la siguiente manera:
from math import sqrt

def hipotenusa(a, b):
    return sqrt(a ** 2 + b ** 2)
NOTA: Todo el código que aquí se presenta fue probado con Python 3.7.

El siguiente código pudiera considerarse una prueba unitaria encargada de verificar que la función hipotenusa devuelva el resultado esperado a partir de las entradas 3 y 4:
if hipotenusa(3.0, 4.0) == 5.0:
    print('Pasó la prueba')
else:
    print('Falló la prueba')
Sin embargo, escribir pruebas similares al ejemplo anterior no es recomendable dado que requiere teclear mucho código cuando deseamos elaborar múltiples pruebas y además no es la manera convencional de hacerlo.

Fuente: searchengineland.com

La distribución estándar de Python provee dos mecanismos que simplifican la escritura y automatización de nuestras pruebas unitarias: unittest y doctest. La primera opción es un módulo inspirado en los frameworks de pruebas unitarias desarrollados por Erich Gamma y Kent Beck para los lenguajes Java y Smalltalk. La segunda opción, doctest, generalmente se considera más sencilla de usar que unittest, aunque esta última puede ser más adecuada para pruebas más complejas.

A continuación realizaré una breve introducción al módulo de doctest con el fin de elaborar pruebas unitarias al momento de definir funciones en Python.

El doctest va dentro del docstring

Un docstring es una cadena de caracteres que se coloca como primer enunciado de un módulo, clase, método o función, con el fin de explicar su intención. Esto lo expliqué hace algunos años con más lujo de detalle en la entrada de este blog titulada Documentando programas en Python.

La función hipotenusa, definida anteriormente, podría tener el docstring que se muestra aquí:
from math import sqrt

def hipotenusa(a, b):
    '''Calcula la hipotenusa de un triángulo rectángulo.

    Utiliza el teorema de Pitágoras para determinar la
    hipotenusa a partir de los catetos de un triángulo.

    Parámetros:
    a -- primer cateto
    b -- segundo cateto

    '''
    return sqrt(a ** 2 + b ** 2)
Aquí el docstring es una cadena de caracteres multi-líneas, la cual comienza y termina con triples comillas sencillas (''') o dobles (""").

El módulo doctest busca en los docstrings fragmentos de texto que parezcan sesiones interactivas de Python con el fin de ejecutarlos y verificar que funcionan exactamente como se muestran.

Por ejemplo, la siguiente sesión interactiva de Python muestra la manera en que se comporta la función hipotenusa con diferentes argumentos:
>>> hipotenusa(3.0, 4.0)
5.0
>>> hipotenusa(0.0, 0.0)
0.0
>>> hipotenusa(8.0, 15.0)
17.0
>>> hipotenusa(39.0, 80.0)
89.0
>>> round(hipotenusa(1.0, 1.0), 4)
1.4142
NOTA: En el último caso se usó la función round para redondear el resultado a 4 cifras después del punto decimal. Se recomienda hacerlo de esta manera para evitar los problemas de precisión que surgen cuando aparecen números reales con una parte fraccionaria compuesta de muchas cifras.

El texto completo de esta sesión interactiva, incluyendo los indicadores (prompts) del intérprete (>>>), se coloca dentro del doctring, típicamente al final, aunque en realidad puede ir donde sea:
from math import sqrt

def hipotenusa(a, b):
    '''Calcula la hipotenusa de un triángulo rectángulo.

    Utiliza el teorema de Pitágoras para determinar la
    hipotenusa a partir de los catetos de un triángulo.

    Parámetros:
    a -- primer cateto
    b -- segundo cateto
    
    Ejemplos de uso:
    
    >>> hipotenusa(3.0, 4.0)
    5.0
    >>> hipotenusa(0.0, 0.0)
    0.0
    >>> hipotenusa(8.0, 15.0)
    17.0
    >>> hipotenusa(39.0, 80.0)
    89.0
    >>> round(hipotenusa(1.0, 1.0), 4)
    1.4142

    '''
    return sqrt(a ** 2 + b ** 2)

Corriendo las pruebas

Existen dos opciones para correr las pruebas. La primera opción consiste en usar la terminal del sistema y ejecutar un comando similar al siguiente  (suponiendo que el archivo que contiene nuestro código se llama pitagoras.py):
python3 -m doctest pitagoras.py
La opción -m doctest le dice al intérprete de Python que ejecute el módulo doctest. El comando no produce salida alguna en caso de pasar todas las pruebas.

Si alguna prueba llega a fallar, entonces se verá un mensaje con la información pertinente. Por ejemplo, si agregamos al doctring el siguiente caso, el cual tiene un resultado incorrecto:
>>> hipotenusa(2.0, 2.0)
2.0
al correr nuevamente las pruebas obtenemos un mensaje similar al siguiente:
************************************************************
File "pitagoras.py", line 25, in 
pitagoras.hipotenusa
Failed example:
    hipotenusa(2.0, 2.0)
Expected:
    2.0
Got:
    2.8284271247461903
************************************************************
1 items had failures:
   1 of   6 in pitagoras.hipotenusa
***Test Failed*** 1 failures.
Se puede añadir también la opción -v (habilitar modo verboso o detallado) en la línea de comando al momento de correr nuestras pruebas. En este caso se produce un reporte completo con la información de todas las pruebas, hayan sido exitosas o no:
python3 -m doctest pitagoras.py -v
La salida en este caso sería:
Trying:
    hipotenusa(3.0, 4.0)
Expecting:
    5.0
ok
Trying:
    hipotenusa(0.0, 0.0)
Expecting:
    0.0
ok
Trying:
    hipotenusa(8.0, 15.0)
Expecting:
    17.0
ok
Trying:
    hipotenusa(39.0, 80.0)
Expecting:
    89.0
ok
Trying:
    round(hipotenusa(1.0, 1.0), 4)
Expecting:
    1.4142
ok
Trying:
    hipotenusa(2.0, 2.0)
Expecting:
    2.0
************************************************************
File "pitagoras.py", line 25, in 
pitagoras.hipotenusa
Failed example:
    hipotenusa(2.0, 2.0)
Expected:
    2.0
Got:
    2.8284271247461903
1 items had no tests:
    pitagoras
************************************************************
1 items had failures:
   1 of   6 in pitagoras.hipotenusa
6 tests in 2 items.
5 passed and 1 failed.
***Test Failed*** 1 failures.
La segunda opción para correr las pruebas consiste en agregar las siguientes tres líneas de código al final del archivo pitagoras.py:
if __name__ == '__main__':
    import doctest
    doctest.testmod()
La instrucción if anterior revisa si el archivo actual se está ejecutando como un programa (cuando la variable __name__ es igual a la cadena '__main__') o si se importó como un módulo (cuando la variable __name__ es igual al nombre del módulo, en este caso la cadena 'pitagoras'). Solo deseamos llevar a cabo las pruebas cuando el archivo se corre como un programa y, cuando es así, realizamos la importación del módulo doctest seguido de la invocación de su función testmod.

Después de lo anterior podemos correr nuestro archivo como nos resulte más conveniente (por ejemplo, desde la terminal o usando nuestro ambiente de desarrollo predilecto). Si es desde la terminal, basta teclear el siguiente comando:
python3 pitagoras.py
Tal como vimos anteriormente, si no se produce salida alguna quiere decir que se pasaron todas las pruebas.

Si queremos ver todo el reporte de pruebas exitosas y fallidas podemos añadir nuevamente la opción -v en el comando de la terminal:
python3 pitagoras.py -v
Alternativamente, el mismo efecto se puede lograr modificando el código para añadir el parámetro opcional verbose con valor de verdadero (True) al momento de invocar la función testmod:
    doctest.testmod(verbose=True)
Ahora, cada vez que el archivo se corra como un programa, sin importar si se utilizó o no la opción -v desde la terminal, se obtendrá el reporte completo con el resultado de todas las pruebas.

Con todas las adecuaciones mencionadas, el archivo pitagoras.py queda en su versión final así:
from math import sqrt

def hipotenusa(a, b):
    '''Calcula la hipotenusa de un triángulo rectángulo.

    Utiliza el teorema de Pitágoras para determinar la
    hipotenusa a partir de los catetos de un triángulo.

    Parámetros:
    a -- primer cateto
    b -- segundo cateto
    
    Ejemplos de uso:
    
    >>> hipotenusa(3.0, 4.0)
    5.0
    >>> hipotenusa(0.0, 0.0)
    0.0
    >>> hipotenusa(8.0, 15.0)
    17.0
    >>> hipotenusa(39.0, 80.0)
    89.0
    >>> round(hipotenusa(1.0, 1.0), 4)
    1.4142
    >>> hipotenusa(2.0, 2.0)
    2.0

    '''
    return sqrt(a ** 2 + b ** 2)

if __name__ == '__main__':
    import doctest
    doctest.testmod(verbose=True)
Este versión del programa se puede correr desde la terminal, tal como ya se explicó, o desde cualquier editor o ambiente de desarrollo para Python (IDLE, Spyder, Visual Studio Code, etc.) utilizando los respectivos mecanismos disponibles para ejecutar código.

Mejores prácticas

Estas son algunas prácticas recomendadas al momento de escribir pruebas unitarias usando doctest:
  • Deben ser lógicamente lo más simples que se pueda.
  • Deben requerir muy poco tiempo para ejecutarse, de preferencia solo unos cuantos milisegundos. Si las pruebas toman mucho tiempo la gente optará por no correrlas.
  • Deben procurar abarcar todos los posibles caminos de ejecución del código. Por ejemplo, para cada instrucción if debe haber al menos un caso para probar cuando la condición es verdadera y otro caso para cuando la condición es falsa.
  • Todas las funciones, módulos, clases y métodos deben tener pruebas unitarias apropiadas.
  • Incluir casos de prueba para las situaciones de error en donde se lancen excepciones.
Veamos otro ejemplo para demostrar el último punto de arriba. Definamos la función reciproco, la cual obtiene el inverso multiplicativo de su argumento:
def reciproco(x):
    return 1 / x
Una sesión interactiva de Python utilizando esta función se muestra a continuación:
>>> reciproco(2)
0.5
>>> reciproco(4)
0.25
>>> reciproco(1)
1.0
>>> reciproco(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "otro_ejemplo.py", line 2, in reciproco
    return 1 / x
ZeroDivisionError: division by zero
La última expresión genera una excepción debido a que Python no permite dividir entre cero.

A partir de la salida de arriba podemos escribir el doctest para la función reciproco. Del mensaje de error solo es necesario conservar dos líneas: la primera (la que comienza con Traceback) y la última (la que contiene el nombre de la excepción y la explicación del error). Por claridad se recomienda eliminar las demás líneas del stack trace.

El docstring completo dentro de su función quedaría así:
def reciproco(x):
    '''Calcula el inverso multiplicativo de x.

    Se cumple que: x * reciproco(x) == 1.0

    Ejemplo de uso:

    >>> reciproco(2)
    0.5
    >>> reciproco(4)
    0.25
    >>> reciproco(1)
    1.0
    >>> reciproco(0)
    Traceback (most recent call last):
    ZeroDivisionError: division by zero
    
    '''
    return 1 / x
Para correr estas pruebas podemos usar cualquiera de las opciones explicadas anteriormente.

Fuente: www.iconarchive.com

Reflexiones finales

¿Qué beneficios ofrece la automatización de las pruebas unitarias? Según quintagroup, algunos ventajas son:
  • Detección temprana de problemas. Las pruebas unitarias permiten encontrar y eliminar errores de manera rápida y oportuna. Es bien sabido que un defecto de software es menos costoso entre más pronto se detecte y elimine.
  • Mitigación del cambio. Se tiene una certeza razonable de que nuestro programa funciona de cierta manera aún después de hacerle modificaciones, ya sea para corregir errores o añadir funcionalidad nueva.
  • Simplificación de la integración. La integración de los diferentes componentes de una aplicación es más sencilla si primero se prueban éstos por separado. 
  • Mejor documentación. Un programador que desea aprender qué funcionalidad proporciona una unidad y cómo usarla puede revisar las pruebas correspondientes para obtener una comprensión básica de su API.
No es un secreto que una clave para desarrollar software de calidad es ir probando el código a medida que se va escribiendo. De hecho, se recomienda primero escribir las prueba antes que el código de la aplicación. A esto se le conoce como desarrollo guiado por pruebas o TDD por sus siglas en inglés. Esta práctica exige que el programador piense más detenidamente sobre el problema a resolver y el diseño que requiere la solución.

Fuente: openclipart.org

Por último, es importante mencionar que la principal limitación de las pruebas unitarias es que no se pueden utilizar para detectar todos los errores en un programa, ya que es imposible evaluar todas sus rutas de ejecución para todos los posibles valores que pueden tomar las variables involucradas, salvo quizás en casos muy triviales. Las pruebas unitarias solo pueden mostrar la presencia o ausencia de errores particulares.

En resumen, las pruebas unitarias son una buena herramienta para facilitar el desarrollo de software de calidad, pero definitivamente no son una bala de plata.

Fuente: magazine.joomla.org

1 de agosto de 2018

Cuando el dictador se jubila

En días pasados recibí un correo electrónico de Jaime Tovar, un destacado ex-alumno mío que actualmente trabaja en el Reino Unido como desarrollador de software sénior. En su mensaje me dio la noticia de que Guido van Rossum había renunciado un día antes a su rol de BDFL (siglas en inglés de “dictador benevolente de por vida”) del lenguaje Python. Jaime me sugirió que escribiera una entrada en este blog para dar a conocer mi opinión al respecto. Inicialmente no estaba muy convencido de que pudiera aportar algún comentario interesante a este hecho, pero después de pensar varios días me di cuenta de que sí tengo algunas reflexiones que puedo compartir con mis apreciables lectores.

Guido van Rossum, creador de Python.
Fuente: www.gacetaholandesa.com

Antecedentes

Guido van Rossum comenzó a escribir la primera implementación del lenguaje Python en diciembre de 1989. Desde aquel entonces, Guido había sido el líder moral indiscutible de la comunidad de Python y el encargado de dirigir la evolución del lenguaje. En 1995, a manera de broma, se le otorgó a Guido el título de “primer BDFL interino”.

Guido (centro) en la primera conferencia de Python en el
Instituto Nacional de Estándares y Tecnología (NIST),
noviembre de 1994 en Gaithersburg, Maryland.
Fuente: legacy.python.org

Pero, ¿qué es exactamente un BDFL? La wikipedia provee la siguiente definición:
BDFL es un título informal que se otorga a ciertos individuos de la comunidad de desarrolladores de software de código abierto que tienen la tarea de asignar las directrices generales y, en ciertas situaciones, las decisiones finales dentro del ámbito de un proyecto. La traducción de Benevolent Dictator for Life es Dictador Benevolente De por vida, lo que conlleva un tanto de informalidad y humor. 
Después de Guido han habido otras personas con el título de BDFL otorgado por parte de sus respectivas comunidades, por ejemplo Linus Torvalds (creador del núcleo o kernel de Linux) y Yukihiro Matsumoto (creador del lenguaje Ruby), por mencionar a un par.

Fuente: image.thmeythmey.com

En su ensayo titulado “Cultivando la noosfera”, Eric Raymond explica cómo la naturaleza del código abierto obliga a que una “dictadura” sea benevolente. Cuando un proyecto se enfrenta a una situación difícil se espera que su BDFL escuche y evalúe pacientemente los argumentos de todas las partes involucradas y finalmente tome la decisión que resulte más conveniente para los intereses de toda la comunidad. Sin benevolencia un desacuerdo mayor puede ocasionar una bifurcación (fork). En este caso una persona o grupo toma el código fuente del proyecto (que al ser abierto cualquiera tiene acceso a él) y comienzan a extenderlo y modificarlo en una dirección distinta a la original. Esto ha ocurrido varias veces en el pasado, por ejemplo cuando MySQL fue bifurcado para crear MariaDB en 2009. Esto fue debido a las preocupaciones que tenían algunos de los desarrolladores principales con respecto a la adquisición anunciada de Sun Micrsoystems (dueños en ese momento de MySQL) por parte de Oracle. La comunidad temía que Oracle, la compañía de bases de datos relacionales más grande del mundo, tuviera como objetivo eliminar eventualmente a MySQL y reducir así su competencia.

Un BDFL usualmente es el autor original de un proyecto. Esto significa que, por lo general, esta persona cuenta con un nivel técnico que otros respetan e incluso admiran. Sin embargo, su destreza tecnológica es tan solo una de varias cualidades que debe tener un buen BDFL. El reto principal de un líder de un proyecto de código abierto es lidiar con personas y coordinarlas, y esto resulta complicado debido a que la mayoría (o incluso todos) son voluntarios y no reciben remuneración económica alguna por su participación. Aquí el respeto, el agradecimiento y el reconocimiento juegan un papel fundamental.

Fuente: www.sieuthichungcu.info

En el año 2009, como broma de April Fool´s Day (equivalente al día de los santos inocentes en muchos países de habla hispana), se coqueteó con la posibilidad de la abdicación de Guido como BDFL. En esa ocasión se presentó el PEP 401 en donde se nombraba a Barry Warsaw como sucesor de Guido. En lugar de BDFL, el título de Barry iba a ser FLUFL (Friendly Language Uncle For Life,  o “tío amistoso del lenguaje de por vida”). Es importante enfatizar que esta noticia tan solo fue una broma de un 1º de abril. Sin embargo, ¿sería esto un augurio de lo que ocurriría casi una década después?

Barry Warsaw, el FLUFL o
“tío amistoso del lenguaje
de por vida”.
Fuente: hwww.linkedin.com

NOTA: Un PEP, o Python Enhancement Proposal, es un documento oficial de la comunidad de Python en el que se informa de una propuesta para mejorar el lenguaje o algún proceso. Posterior a la publicación de un PEP, los miembros de la comunidad discuten sobre la viabilidad de la propuesta y ofrecen sugerencias. La decisión final para aprobar un PEP corre a cargo del BDFL o, en su defecto, por algún delegado del BDFL. Todos los cambios relevantes que ocurren en Python comienzan con uno o varios PEPs. Tristemente, como veremos en un momento, fue a consecuencia de las reacciones negativas que recibió después de aceptar un PEP que Guido optó por anunciar su retiro como BDFL.


La noticia

El pasado jueves 12 de julio, Guido van Rossum comunicó lo siguiente a través de la lista de correo de python-committers:
Asunto: Transferencia de poder

Ahora que está concluido el PEP 572 ya no quiero tener que pelear por un PEP y descubrir que mucha gente detesta mis decisiones.

Quiero descartarme por completo del proceso de toma de decisiones. Seguiré allí por un tiempo como cualquier otro desarrollador del núcleo de Python, y todavía estaré disponible para guiar a las personas, posiblemente ahora con mayor disponibilidad. Pero básicamente me estoy dando unas vacaciones permanentes del puesto de BDFL, y ahora todos ustedes estarán por su propia cuenta.

Esto iba a suceder eventualmente de cualquier manera. Aún está ese autobús al acecho a la vuelta de la esquina, y no me estoy haciendo más joven... (evitaré compartirles mi lista de problemas médicos).

No voy a nombrar un sucesor.

Entonces, ¿qué van a hacer? ¿Crear una democracia? ¿Una anarquía? ¿Una dictadura? ¿Una federación?

No me preocupan las decisiones cotidianas que surgen del sistema de seguimiento de incidentes o desde GitHub. Raramente solicitan mi opinión, y por lo general no es realmente importante. Así que esto continúa como siempre.

Las decisiones que más importan probablemente son:
  • ¿Cómo se deciden las PEPs?
  • ¿Cómo iniciar a los nuevos desarrolladores del núcleo de Python?
Es posible que podamos escribir procesos para estas cosas vía PEPs (quizás dichas PEPs podrían conformar una especie de constitución). Pero hay un truco aquí. Voy a dejar que todos ustedes (los autores de los “commits”) lo resuelvan por sí mismos.

Tengan en cuenta que aún existe un código de conducta. Si no les gusta dicho documento, la única opción podría ser dejar este grupo voluntariamente. Quizás hay detalles aún por decidir, por ejemplo: ¿cuándo debería ser expulsada alguna persona? (esto implica restringirle el acceso a las listas de correo de python-dev o python-ideas, ya que éstas también están cubiertas por el código de conducta).

Finalmente, un recordatorio de que los registros de esta lista son públicos (https://mail.python.org/pipermail/python-committers/) aunque la membresía es restringida (limitada solamente a los desarrolladores del núcleo de Python). Todavía estaré aquí, pero estoy intentado dejar que todos ustedes resuelvan algo por su propia cuenta. Estoy cansado y necesito un descanso muy largo.

— Guido van Rossum (python.org/~guido)
El PEP 572 al que Guido se refiere al inicio de su comunicado es sobre “Expresiones de asignación”, un cambio previsto para ser incorporado en la versión 3.8 del lenguaje que está programada a ser liberada en octubre de 2019. Actualmente, las asignaciones a variables en Python deben realizarse usando un enunciado (statement) de asignación. Veamos el siguiente ejemplo:
# Código para Python 3.0 y superior
r = input('¿Deseas continuar? (s/n) ')     # 1  
while r in 'Ss':                           # 2
    print('Tu respuesta fue:', r)          # 3
    print('Continuando...')                # 4
    r = input('¿Deseas continuar? (s/n) ') # 5
Las líneas 1 y 5 son enunciados de asignación. De hecho, son exactamente el mismo enunciado. Esta duplicación es necesaria aquí dado que Python no cuenta con un enunciado do-while y el valor de la variable r es requerida tanto en la condición (línea 2) como en el cuerpo (línea 3) del enunciado while.

El PEP 572 introduce un nuevo operador de asignación := (un signo de dos puntos seguido de un signo de igual) que permite realizar asignaciones dentro de una expresión. Este nuevo operador es esencialmente equivalente al operador de asignación = (un signo de igual) soportado por los lenguajes basados en C (C++, C#, Java, JavaScript, etc.). Ahora con el operador de asignación := el código de arriba podría quedar escrito de manera más compacta así:
# Código para Python 3.8
while (r := input('¿Deseas continuar? (s/n) ')) in 'Ss': # 1
    print('Tu respuesta fue:', r)                        # 2
    print('Continuando...')                              # 3
Hay que notar que la asignación de r se realiza ahora dentro de la misma expresión condicional del enunciado while (línea 1) reduciendo con ello dos líneas del programa. Cada vez que se realiza la condición del while, se realiza primero la re-asignación de la variables r.

A muchas personas no le gustó esta adición al lenguaje. Hubo a quienes simplemente no les gustó la sintaxis del nuevo operador. A muchos otros no les gustó en absoluto la idea de tener una forma alternativa de realizar asignaciones de variables por considerar que estaba quebrantando el Zen de Python, el cual es una serie de principios de software que influyen en el diseño del lenguaje.

La gente en desacuerdo comenzó a compartir su opinión al respecto con poca mesura, tanto a Guido como a la Internet. Y así fue como se abrió esta pequeña caja de Pandora.

Pandora's Box by Arthur Rackham
“Caja de Pandora” de Arthur Rackham.
Fuente: talesfortadpoles.ie

Francamente, a mí no me gustó al principio la propuesta ni la sintaxis seleccionada. Después de algunos días de asimilación me acostumbré a la idea y creo que sí utilizaría este nuevo operador, aunque solo en ciertos contextos. Sin embargo, como educador, lo más probable es que optaría por no enseñarlo a mis alumnos, al menos no durante un curso de introducción a la programación. La razón es simple: idealmente cada enunciado debe tener un solo efecto. Si un enunciado tiene dos o más efectos resulta más difícil de entender. Es preferible que cada efecto esté en su propia línea. En el último código, la línea 1 tiene dos efectos: la asignación y el resultado de la condición del ciclo. Esta complejidad no la necesita un principiante, y por tanto me parece más conveniente evitarla. Y así es como enseñamos a programar: evitando presentar a nuestros estudiantes aquellos elementos del lenguaje que no contribuyen a lograr nuestros objetivos académicos.

Reflexiones

Antes que todo, pienso que es una pena que Guido haya abandonado su puesto como BDFL. Sin embargo entiendo y respeto completamente su decisión y no puedo mas que sentir un profundo agradecimiento por todo el tiempo y la dedicación que invirtió para lograr lo que actualmente es Python y su vibrante comunidad. También hay que recalcar que Guido no va a esfumarse. De hecho, él continuará presidiendo la Python Software Foundation (PSF), que es la organización dedicada a fomentar el desarrollo de la comunidad de Python. En la biografía de su cuenta de Twitter, Guido ahora se describe como “Creador de Python y BDFL emérito”.

Es válido que la gente esté en desacuerdo con nuestras ideas y es muy valioso que podamos discutir, pero siempre y cuando lo hagamos de manera inteligente y respetuosa. Lamentablemente, en muchas ocasiones lo anterior es la excepción y no la regla. Lo que ocurrió a raíz del PEP 572 es un fenómeno que estamos viendo en otros lados. Al parecer todo el mundo se siente con la libertad de expresar su opinión y ahora, gracias a la Internet y a las redes sociales, tenemos a nuestra disposición un público de un tamaño que jamás nos hubiéramos imaginado tan solo hace algunos años. En principio esto parece algo bueno, ya que podría ser la base para una verdadera democratización, en donde todos pueden hacer oír su voz. Pero lo que ha pasado en muchos casos es que los que hablan y gritan más fuerte son los que han estado dominado el escenario. Muchas personas se sienten con todo el derecho de vociferar su opinión, no importándoles si sus argumentos cuentan o no con un sustento razonable. ¿A caso ser escandaloso tiene más peso que ser instruido?

Fuente: www.vexels.com

Un número desmedido de personas repudiaron la decisión de Guido de aprobar el PEP 572 y se lo manifestaron de manera hiriente. En una entrevista realizada por InfoWorld hace unos días, Guido comentó que sentía que había perdido la confianza de los desarrolladores del núcleo de Python cuando varios de ellos le atacaron de manera personal en redes sociales como Twitter. Al igual que la mayoría de esos desarrolladores, el rol de BDFL que desempeñaba Guido era también a título de voluntario. ¿Qué puede ser más desmoralizante que hacer un trabajo de manera generosa y a cambio de ello recibir severas críticas y agresiones?

Regresemos ahora al tema de las dictaduras benevolentes. Si vivimos en un país democrático muy probablemente consideramos que la democracia es el estilo de gobierno que nos conviene emular en otros entornos de nuestra sociedad. Pero la democracia no funciona en todos los contextos. Imaginemos un hogar conformado por cinco personas: tres hijos pequeños, la mamá y el papá. Si cada mañana deciden entre todos el menú para desayunar de manera democrática existe una alta probabilidad de que acabarán comiendo siempre helado y hot cakes. La verdad es que unos buenos padres procurarán brindar una alimentación balanceada a sus hijos, no porque quieran fastidiarlos dándoles comida que a lo mejor no les gusta, sino porque buscan su bienestar a largo plazo. En otras palabras, los padres juegan el rol de dictadores benevolentes. Unos padres amorosos seguramente escucharán y tomarán en cuenta las opiniones de sus hijos, pero finalmente tomarán decisiones que no siempre serán populares ni bien recibidas.

Fuente: www.shutterstock.com

La comunidad de Python decidió desde sus inicios que su forma de gobierno iba ser a través de un dictador benevolente. La idea era contar con una persona sabia y visionaria que pudiera tomar las decisiones difíciles, cuidando siempre los intereses de la propia comunidad. Ése era el acuerdo. Inicialmente, para cada situación importante, iba a haber un espacio para discutir los diferentes puntos de vista. Pero al final Guido tomaría la última decisión y todos tendrían que acatarla. Guido era el capitán del barco, y se le otorgó a él la confianza y el permiso para dirigir la nave de acuerdo a su criterio. Se estableció desde el principio que esto no iba funcionar como una democracia. Por lo tanto, una vez tomada una decisión por parte del BDFL ya no queda nada más que hacer, salvo apechugar en caso de que no fuera de nuestro agrado. Al parecer no todo el mundo entiende cómo funciona este estilo de gobierno. Después de que Guido anunció su decisión sobre el PEP 572 muchos inconformes sintieron que estaban en su derecho de lanzarse a la yugular del BDFL.

He podido de ver de primera mano que Guido tiene una personalidad conciliadora y no busca imponerse. Esto contrasta con otros BDFLs, como es el caso de Linus Torvalds, quien no tiene problemas en mandar al carajo a quienes no comparten su punto de vista. Incluso Torvalds alguna una vez dijo: “No soy una persona amable, y tú no me interesas. Me importa la tecnología y el núcleo de Linux; eso es lo importante para mí”. Pero Guido no es así. Él es un individuo gentil que busca llevarse bien con la gente y que pone a las personas por encima de la tecnología. Es probable que esa forma afable de ser lo llevaría a ir acumulando a través de los años una enorme presión que finalmente terminaría por reventarlo.

Lo que mucha gente se pregunta ahora es: ¿qué va a pasar con Python? ¿Qué va a ocurrir si se decide que el lenguaje evolucione bajo un esquema en donde mucha gente pueda meter su cuchara? Esto preocupa porque viene a la mente aquel viejo proverbio inglés que dice: “Demasiados cocineros echan a perder el caldo”.

https://d12m9erqbesehq.cloudfront.net/wp-content/uploads/sites/21338/2018/04/12132631/cooking.jpg
Fuente: northwoodelementary.eventsmart.com

En el pasado, algunos lenguajes de programación han sido diseñados y extendidos mediante comités conformados por múltiples compañías e individuos. Ese fue el caso de la estandarización de Common Lisp. Cada miembro del comité tenía su propia visión de lo que consideraba que debía incluir el lenguaje. El problema fue que las visiones individuales no necesariamente coincidían entre sí. Si alguien quería que se incorporara una determinada característica en el lenguaje a menudo tenía que realizar concesiones con los otros miembros del comité. Así pues quedó finalmente un lenguaje gigantesco (la especificación original consistía de más de mil páginas) y chipotudo (con inconsistencias y redundancias). Bien dijo el ingeniero automotriz británico Alec Issigonis (creador del auto Mini original): “un camello es un caballo diseñado por un comité”.

Fuente: medium.com

Otro problema que tienen los comités es que frecuentemente tardan mucho tiempo en generar resultados, sobre todo si el número de integrantes es grande. Podemos observar esta situación en la evolución que ha tenido el lenguaje Java comparada con el lenguaje C#. En el año 1995, Sun Microsystems liberó la primera versión de Java. Por su parte, Microsoft sacó C# al mercado siete años después. Aunque inicialmente C# fue denunciado por muchos como una copia deficiente de Java, al paso de los años esta situación cambió. Java poco a poco se fue rezagando a tal grado que ahora es Java quien le está copiando a C#. Por ejemplo, las funciones anónimas (conocidas también como lambdas) aparecieron en C# 3.0 en 2007, pero fue hasta el 2014 que fueron incorporadas a Java 8. El lenguaje Java tarda más tiempo en avanzar debido a que depende del Proceso de la Comunidad Java (JCP por sus siglas en inglés), que está subordinado a un comité relativamente extenso que se encarga de tomar todas las decisiones relacionadas con la plataforma Java. Por otro lado, el rápido y ágil progreso de C# ha sido producto de un pequeño equipo encabezado por el eminente ingeniero de software danés Anders Hejlsberg (creador de Turbo Pascal, Delphi y C#).

¿Tomará Python el camino de un estilo de gobierno encabezado por un comité? En la entrevista de InfoWorld previamente mencionada, Guido comentó lo siguiente:
El grupo de desarrolladores del núcleo de Python acordó una agenda para llegar a una conclusión. La fecha límite para las propuestas es el 1° de octubre de 2018. Entonces, me parece, que para el 1° de noviembre de 2018 se han comprometido a haber seleccionado una propuesta para una estructura de gobierno. Luego, para el 1° de enero de 2019, se comprometieron a elegir o designar efectivamente, o como sea que se indique en sus documentos de gobierno, a las personas que estarán a cargo.

Si una de las propuestas es que haya un solo BDFL, esa propuesta deberá redactarse detalladamente
para el 1° de octubre, indicando cómo se seleccionará, cuánto tiempo permanecerá a cargo, cómo se le puede impugnar, y todo eso. Tal vez para el 1° de enero tendrán una persona real designada.
En un podcast de PythonBytes, Carol Willing y Brett Cannon, dos de los desarrolladores del núcleo de Python, comentaron cómo creen ellos que va a quedar conformado el nuevo modelo de gobierno para Python. Al parecer se está pensando en que haya un nuevo BDFL, o alternativamente un “consejo de ancianos” compuesto por entre 3 y 5 personas. Esto me parece una muy buena noticia por la razón que ya expuse anteriormente.

Fuente: maxima.id

Haciendo a un lado la dimisión de Guido como BDFL, hay que reconocer que Python está viviendo su mejor momento y dudo que esto vaya a cambiar en el futuro inmediato. Según Stack Overlow, de los principales lenguajes de programación, Python ha sido el que más ha crecido en los últimos cinco años dentro de los países con mayores ingresos. El índice TIOBE de julio de 2018 coloca a Python como el cuarto lenguaje de programación más popular del momento, solo atrás de Java, C y C++. Por otra parte, Python aparece en el primer puesto tanto en el índice de popularidad de lenguajes de programación (PyPL) de julio 2018 como en la clasificación 2018 de los principales lenguajes de programación de la revista IEEE Spectrum.

Una gran parte de la comunidad de Python está conformada por usuarios que no son desarrolladores de software pero que sí necesitan programar para resolver problemas de cómputo científico, ciencia de datos y aprendizaje automático. Estos usuarios dependen de herramientas basadas en Python como Jupyter, NumPy, SciPy, Matplotlib, pandas y TensorFlow. No veo que el retiro de Guido les pudiera afectar, incluso si la evolución del lenguaje se detuviera por algunos años. La inmensa comunidad de Python está muy bien consolidada y eso permitirá que trascienda más allá de una sola persona. Desde luego que vamos a extrañar al buen Guido, nuestro dictador benevolente, pero me siento optimista sobre el destino de Python.

Con Guido, BDFL emérito.
Foto tomada durante PyCon USA, mayo de 2018.

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.

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.