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.

5 comentarios:

  1. Interesante su trabajo profesor, muchas gracias por aclarar el punto de Python 2 y 3. Para mi Python 3 esta usando una sintaxis mas clara en cuanto a definicion de variables y sus formas de integrar variables anidadas.

    Saludos y siga siendo chevere!!!

    ResponderEliminar
  2. Muy buen artículo...
    No dudo que la respuesta adecuada a esas dudas que mencionas es el punto 3:
    "¿Debemos enseñar Python 3, viendo hacia adelante, favoreciendo lo que pueden ser las ideas y prácticas del futuro? "

    ¿quieres saber por qué llegué a este artículo?
    Te cuento lo que me pasó y te darás cuenta...
    (resumido, claro...)

    Situación: en cadenas tenía la sig. info:
    # coding=UTF-8
    "1 FER" # el 1 es la calle Fernandez
    "2 GAR" # el 2 ... García
    ...
    # Que codigo de calle es la 2?
    # busco y encuentro
    calle = "002 GAR"
    print calle[-3:]
    "GAR"
    # OK
    ...
    # Que codigo de calle es la 1?
    # idem
    calle = "001 FER"
    print calle[-3:]
    "FER"
    # OK
    ...
    # apareció una nueva calle con los datos:
    "126 NÑZ" # 126 es la calle Nuñez
    calle = "126 NÑZ"
    print calle[-3:]
    '\xc3\x91Z'
    # opss la Ñ me comió la N
    Decidido, basta de líos como éstos: me paso a python3!
    Saludos
    Rubén

    ResponderEliminar
  3. Excelente Blog. Personalmente considero que Python es hoy por hoy el mejor lenguaje para la Educación
    http://pythonscouts.cubava.cu

    ResponderEliminar