Automágica: durante 2017 estoy trabajando bastante en Automágica, mi software para editar libros: Más información - Posts relacionados

La historia de Python: Los orígenes de las características "Funcionales" de Python

El siguiente texto es una traducción del artículo Origins of Python's "Functional" Features de Guido van Rossum publicado en http://python-history.blogspot.com/.

Los orígenes de las características "Funcionales" de Python

Yo nunca considere que Python esté fuertemente influenciado por lenguajes funcionales, no importa lo que la gente diga o piense. Estoy  mucho mas familiarizado con lenguajes imperativos, como C y Algol 68, y aunque hice a las funciones objetos de primera clase, nunca vi a Python como un lenguaje de programación funcional. Sin embargo, tiempo atrás, tenia claro que los usuarios querían hacer mucho mas con las  listas y las funciones

Una operación común en las listas fue la de mapear una función a cada elemento de una lista, creando una nueva lista. Por ejemplo:

def square(x):

    return x*x

vals = [1, 2, 3, 4]

newvals = []

for v in vals:

    newvals.append(square(v))

En los lenguajes funcionales como Lisp y Sheme, operaciones como esa son provistas como funciones incorporadas al lenguaje. Por lo tanto, los nuevos usuarios, familiarizados con este tipo de lenguajes se encontraron a si mismos implementando funcionalidades similares en Python. Por Ejemplo

def map(f, s):

    result = []

    for x in s:

        result.append(f(x))

    return result
def square(x):

    return x*x

vals = [1, 2, 3, 4]

newvals = map(square,vals)

Un detalle sutil del código de arriba es que a mucha gente no le gusto la idea de tener que definir una función separada para la operación  que estaban aplicando a cada elemento de la lista. Lenguajes como Lisp permitían funciones simplemente definidas "al vuelo", al hacer la llamada a la función map. Por ejemplo en Scheme, se pueden crear funciones anónimas y hacer operaciones de asignación en una expresión simple usando lambda, de esta forma:

(map (lambda (x) (* x x)) '(1 2 3 4))

Aunque en Python las funciones son objetos de primera clase, no tenia un mecanismo similar para para crear funciones anónimas.

A finales de 1993, los usuarios estaban proponiendo varias ideas para crear funciones anónimas y funciones para manipular listas como: map(), filter() y reduce().  Por ejemplo, Mark Lutz (autor de "Programming Python") envió este código, para una función que  crea funciones usando exec:

def genfunc(args, expr):

    exec('def f(' + args + '): return ' + expr)

    return eval('f')
# Sample usage

vals = [1, 2, 3, 4]

newvals = map(genfunc('x', 'x*x'), vals)

Entonces Tim Peters lo siguió con una solución que simplificaba un poco mas la sintaxis, permitiendo que los usuarios escriban:

vals = [1, 2, 3, 4]

newvals = map(func('x: x*x'), vals)

Estaba claro que había una demanda de esas funcionalidades. Sin embargo, al mismo tiempo,  me parecía demasiado "hacky" que se creen funciones anónimas como strings, que tenías que procesar usando "exec". Así que en enero de 1994, las funciones map(), filter(), y reduce() fueron agregadas a la biblioteca estándar. Ademas se creo el operador lambda para crear funciones anónimas (como expresión) con una sintaxis mas sencilla. Por Ejemplo:

vals = [1, 2, 3, 4]

newvals = map(lambda x:x*x, vals)

Esas incorporaciones representan una significativa  contribución de código fuente. Desafortunadamente no recuerdo el autor, y  no esta registrado en el SVN. Si es tuyo, ¡dejá un comentario!

Nunca estuve del todo conforme con el uso de la termino "lambda", pero a falta de una mejor y mas obvia alternativa, fue lo adoptado para Python. Después de todo, fue la elección del contribuyente anónimo, y en ese momento los grandes cambios requerían menos discusión que actualmente, para bien o para mal.

Lambda solo pretendía ser una herramienta sintáctica para definir funciones anónimas. Sin embargo, la elección de esa terminología tuvo muchas consecuencias inesperadas. Los usuarios acostumbrados a los lenguajes funcionales esperaban que la semántica fuese igual que en estos. Como resultado, encontraban que la implementación de Python no tenia demasiadas características avanzadas. Un detalle de lambda es que la expresión no puede referirse a variables en el ámbito circundante. Por  ejemplo, si tenes el siguiente código, la función map() se rompería, porque la función lambda se ejecutaría con una referencia indefinida a la variable "a".

def spam(s):

    a = 4

    r = map(lambda x: a*x, s)

Había formas de solucionar este problema, pero involucraban prácticas ilógicas como setear argumentos por defecto y pasar argumentos escondidos en la expresión lambda. Por ejemplo:

def spam(s):

    a = 4

    r = map(lambda x, a=a: a*x, s)

La solución "correcta" a este problema fue que las funciones interiores llevaran, implícitamente, referencias a todas las variables locales del entorno circundante referenciadas por la función. Esto es conocido como "closure", y es un aspecto esencial de los lenguajes funcionales. Sin embargo esa capacidad no se introdujo hasta la versión 2.2 (pero podia ser importada "desde el futuro" en Python 2.1).

Curiosamente, map, filter y reduce, que motivaron originalmente la introducción de lambda y otras características funcionales, fueron, en gran medida reemplazadas por las listas por comprensión y las expresiones generadoras. De hecho, la función reduce fue removida de las funciones incorporadas en Python 3.0 (Sin embargo no es necesario que me manden quejas por la quita de lambda, map o filter: se quedarán en su sitio :-)

Hay que tener en cuenta que, aunque yo no preveía a Python como un lenguaje funcional, la introducción de closures ha sido útil en el desarrollo de muchas otras características avanzadas de programación. Por ejemplo, ciertos aspectos de los nuevos estilos de clases, decoradores y otras funcionalidades dependen de esta característica.

Finalmente, aunque a lo largo de los años se introdujeron varias características de programación funcional, Python no tiene ciertas capacidades que se encuentran en los verdaderos lenguajes de programación funcional. Por ejemplo, Python no realiza ciertos tipos de optimizaciones (como recursión por la cola). En general, por la naturaleza extremadamente dinámica de Python, es imposible de hacer optimizaciones en tiempo de compilación como las de lenguajes como Haskell o ML. Y eso es bueno.

Traducido por Joaquín Sorianello.

Revisado por Juan José Conti.

Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.


La historia de Python: El Gran (o Enorme) Renombrado

El siguiente texto es una traducción del artículo The Great (or Grand) Renaming de Guido van Rossum publicado en http://python-history.blogspot.com/.

El Gran (o Enorme) Renombrado

Cuando creé Python, siempre lo imagine como un programa autónomo, enlazado ocasionalmente con bibliotecas de terceros. Por lo tanto, en el código fuente, se definían nombres globales con total libertad, como "object", "getlistitem", "INCREF" y muchos otros mas. Cuando la popularidad de Python se incrementó, la gente comenzó a pedir una versión "embebida", que fuera también una biblioteca enlazable a otras aplicaciones - de una forma similar en la que Emacs incorpora un interprete de Lisp.

Desafortunadamente, la integración se complicaba por conflictos entre los nombres globales de Python y los definidos por la aplicación - "object" era especialmente popular. Para lidiar con ese problemas se eligió una convención, por la cual todos los nombre globales comenzarían con "Py" o "_Py" (para los internos que tenían que ser globales por razones técnicas) o "PY" (para las macros).

Por razones de compatibilidad hacia atrás (ya que había muchos módulos de extensión de terceros) y para facilitar la transición a los desarrolladores del núcleo (que tenían los viejos nombres enquistados en sus mentes) hubieron dos fases. En la fase uno, el enlazador aceptaba los nombres antiguos, pero el código fuente usaba los nuevos, que eran traducidos a los antiguos usando muchas macros del pre procesador de C. En la fase dos, el enlazador veía los nuevos, pero, para beneficio de los módulos de extensión que todavía no habían sido portados, otro conjunto de macros traducía los viejos a los nuevos. En ambas fases el código podía incluir ambos, y funcionar correctamente.

Investigué un poco la historia en los logs de Subversion. Encontré la revisión r4583 del 12 de enero de 1995, que marca el comienzo de la fase dos, introduciendo los nuevos nombres a los archivos de encabezado. Pero en diciembre de 1996 el renombrado de los archivos fuentes ".c" seguia en marcha. En ese momento el renombrado parecía haber cambiado de nombre, los comentarios de registro lo llamaban "El Enorme Renombrado". Las macros de compatibilidad hacia atras fueron finalmente removidos en mayo de 2000, como resultado de la liberación 1.6. El comentario de r15313 celebra este evento.

La mayor parte del crédito se lo llevaron Barry Warsaw y Roger Masse, que participaron en la desagradable tarea de renombrar los contenidos de archivo, tras archivo, tras archivo... (aunque con la ayuda de un script). También ayudaron en la tediosa tarea de agregar test unitarios para gran parte de la biblioteca estándar.

Wikipedia hace referencia a un anterior Gran Renombrado, que aparentemente consistió en el renombre de grupos de USENET. Probablemente lo recordaba de forma inconsciente cuando lo llamé asi. También encontré algunas referencias a un Gran Renombrado posterior en Sphinx, el paquete utilizado para generar la documentación de Python. Zope también tuvo uno, y algunos debates recientes de Py3k utilizan el término para el cambio de PyString a PyBytes (aunque es menor, comparado con los otros).

Los Grandes o Enormes cambios de nombres son a menudo eventos traumáticos para las comunidades de desarrollo de software, porque requieren que los cerebros de los programadores sean recableados, la documentación reescrita y se complica la integración de parches creados antes, pero aplicados después (esto es especialmente problemático cuando existen ramas no renombradas).

Traducido por Joaquín Sorianello.

Revisado por Juan José Conti.

Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.


La historia de Python: Módulos cargados dinámicamente

El siguiente texto es una traducción del artículo Dynamically Loaded Modules de Guido van Rossum publicado en http://python-history.blogspot.com/.

Módulos cargados dinámicamente

La arquitectura de Python permitió, desde un principio, escribir módulos de extensión escritos en C de una forma sencilla. Sin embargo, en los primeros días, la tecnología de carga dinámica era tan oscura que las extensiones tenían que ser enlazadas estáticamente en el interprete de Python, en tiempo de compilación. Para hacer esto, lo módulos de extensión tenían que ser agregados a un script de shell que era usado para generar el Makefile para Python y todos su módulos de extensión.

Aunque este enfoque funcionaba para pequeños proyectos, la comunidad de Python comenzó a producir nuevos módulos de extensión a un ritmo no esperado, y demandaban que los módulos pudiesen ser compilados y cargados en forma separada. Poco después, las interfaces a las APIs de enlace dinámico, propias de cada plataforma, permitieron que la declaración import busque una biblioteca compartida en disco, de forma similar que un archivo ".py". La primera mención de la carga dinámica en los logs del CVS, data de enero de 1992 y el soporte para la mayoría de las plataformas llego a fines de 1994.

El soporte de enlace dinámico probó ser muy útil, pero fue una pesadilla de mantener. Cada plataforma usaba una API diferente y algunas plataformas tenían adicionales. En enero de 1995, el soporte para enlace dinámico fue reestructurado, de forma tal que todo el código fue concentrado en un solo archivo fuente. Sin embargo, este enfoque resulto en un largo archivo abarrotado de directivas condicionales de compilación (#ifdef). En diciembre de 1999, fue reestructurado de nuevo, con la ayuda de Greg Stein, para que el código correspondiente a cada plataforma quede ubicado en un archivo específico para cada una (o familia de estas).

Aunque Python soportaba la carga dinámica de módulos, el procedimiento para construirlos, a menudo seguía siendo un misterio para muchos usuarios. Un número cada vez más grande de usuarios fueron construyendo módulos, especialmente con la introducción de herramientas como SWIG. No obstante, un usuario deseoso de distribuir un modulo de extensión enfrentaba grandes obstáculos para lograr que el modulo compile en todas las combinaciones de plataformas, compiladores y linkers. En el peor escenario posible, un usuario tenía que escribir su propio Makefile y script de configuración para establecer los flags correctos para el compilador y el linker. Además, requería que los usuarios finales tuviesen una distribución de Python con el código fuente.

Finalmente, se creo una herramienta para construir las extensiones, llamada distutils, que permitió construir e instalar los módulos de extensión en cualquier plataforma. Las opciones necesarias para el linker y el compilador están escritas desde el makefile de Python a un archivo de datos, que es consultado por distutils cuando construye módulos de extensión. Escrito en gran parte por Greg Ward, las primeras versiones de distutils fueron distribuidas en forma separada, para dar soporte a versiones viejas de Python. Desde Python 1.6 está integrado en las distribuciones, como un modulo de la biblioteca estándar.

Cabe destacar que distutils hace mucho mas que simplemente construir módulos de extensión desde código fuente en C. Puede también instalar módulos y paquetes de Python puro, crear instaladores ejecutables para Windows y correr herramientas de terceros como SWIG. Desgraciadamente, su complejidad ha causado que sea maldecida por mucha gente y no reciba la atención que se merece a la hora de mantenerla. Como resultado, recientemente, las alternativas de terceros ( especialmente ez_install, también llamada "eggs") se hicieron mas populares, desgraciadamente causando fragmentación en la comunidad de desarrolladores, así como quejas cuando no funcionan. Parece que el problema en toda su generalidad es inherentemente difícil.

Traducido por Joaquín Sorianello.

Revisado por Juan José Conti.

Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.


La historia de Python: El problema con la división entre enteros

El siguiente texto es una traducción del artículo The Problem with Integer Division de Guido van Rossum publicado en http://python-history.blogspot.com/.

El problema con la división entre enteros

La forma en que Python maneja la división entre enteros es un ejemplo de errores iniciales con enormes consecuencias. Como se mencionó anteriormente, cuando Python fue creado abandoné el enfoque con el que ABC abordaba los números. Por ejemplo, en ABC, cuando dividías dos enteros, el resultado era un número racional exacto que representaba el resultado. En Python, sin embargo, la división entre enteros trunca el resultado a un entero.

En mi experiencia, los números racionales no son tan buenos como los diseñadores de ABC esperaban. Una experiencia típica podría ser escribir un programa simple con alguna aplicación comercial (digamos, calcular tus impuestos) y encontrar que corre más lento de lo esperado. Luego de depurar se encuentra la causa: internamente el programa usa números racionales con miles de dígitos de precisión para representar valores que serán truncados a dos o tres dígitos al ser impresos. Esto podía ser fácilmente solucionado empezando una suma con un cero inexacto, pero esto era a menudo no intuitivo y difícil de depurar para los principiantes.

Entonces en Python utilicé el otro modelo numérico que me era familiar, C. C tiene enteros y números de punto flotante de varios tamaños. Entonces elegí representar los enteros de Python con longs de C (garantizando por lo menos 32 bits de precisión) y los números de punto flotante con doubles de C. Luego añadí un tipo entero con precisión arbitraria que llamé "long".

El mayor error fue que también tomé prestada una regla que tenía sentido en C pero no en un lenguaje de tan alto nivel. Para las operaciones aritméticas estándares, incluyendo la división, el resultado siempre sería del mismo tipo que los operandos. Para empeorar las cosas, inicialmente usé otra regla equivocada que prohibía usar aritmética mixta, con la idea de hacer que las implementaciones de los distintos tipos sean independientes entre sí. Entonces, en un principio no se podía sumar un int con un float, o incluso un int con un long. Luego de que Python se distribuyera públicamente, Time Peters rápidamente me convenció de que esto era realmente una mala idea e introdujo una aritmética mixta con las reglas de coerción típicas. Por ejemplo, mezclar un operando int y uno long convertiría el argumento de tipo int a long y retornaría un long como resultado y la mezcla con un float convertiría al argumento int o long en un float y retornaría un resultado float.

Desafortunadamente, el daño estaba hecho: la división entre enteros daba un resultado entero. Estarás pensando "¿por qué era esto tan malo?". ¿Estaba haciendo escándalo por nada? Históricamente, la propuesta para cambiar esto ha tenido algunas oposiciones duras por parte de quienes pensaban que aprender división entre enteros era una de los más útiles "ritos de iniciación" para todos los programadores. Así que déjenme explicarles las razones por la cual considero esto un error de diseño.

Cuando escribes una función para implementar un algoritmo numérico (por ejemplo, calcular las fases de la luna) esperas que los argumentos sean números de punto flotante. Sin embargo, ya que Python no tiene declaración de tipos, nada evita que la función sea llamada con argumentos enteros. En un lenguaje estáticamente tipado, como C, el compilador convertiría los argumentos a floats, pero Python no sabe nada de eso; el algoritmo corre con valores enteros hasta que las maravillas de la aritmética mixta produzca resultados intermedios que sean floats.

Para todo excepto para la división, los enteros se comportan de la misma forma que sus números de punto flotante correspondiente. Por ejemplo, 1+1 es igual a 2 de la misma forma que 1.0+1.0 es igual a 2.0, y así. Por lo tanto uno puede fácilmente confundirse y esperar que los algoritmos numéricos funcionen independientemente de si son ejecutados con argumentos enteros o de punto flotante. Sin embargo, cuando hay una división, y existe la posibilidad de que los dos operandos sean enteros, el resultado numérico es truncado silenciosamente, esencialmente introduciendo un gran error potencial en el cómputo. Aunque uno puede escribir código defensivo que convierta todos los argumentos a float apenas se introducen, esto es tedioso, y no mejora la legibilidad o mantenibilidad del código. Adicionalmente, evita que el algoritmo sea usado con números complejos (aunque eso sería en casos muy especiales).

Nuevamente, todo esto es un problema porque Python no convierte argumentos automáticamente a algún tipo declarado. Pasar un argumento inválido, por ejemplo un string, es generalmente atrapado rápido por que muy pocas operaciones aceptan mezclar operados strings/números (siendo la excepción la multiplicación). Sin embargo, pasar un entero puede causar una respuesta cercana a la correcta, pero con error; difícil de depurar o incluso advertir (esto me pasó recientemente en un programa que dibuja un reloj analógico; las posiciones de las manecillas eran calculadas incorrectamente debido al truncamiento, pero el error era apenas detectable excepto en ciertas horas del día).

Arreglar la división entre enteros no fue una tarea fácil debido a los programas que dependían de este truncamiento. Se añadió al lenguaje un operador de división truncada (//) que provee la misma funcionalidad. Además, se introduzco un mecanismo ("from __future__ import division") para habilitar fácilmente la nueva semántica para la división entre enteros. Finalmente, se agregó una bandera para la línea de comando (-Qxxx) para cambiar el comportamiento y para asistir la conversión de programas. Afortunadamente, el comportamiento correcto se convirtió en el comportamiento por defecto en Python 3000.

Traducido por Juan José Conti.
Revisado por César Portela.
Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.


La historia de Python: Cómo las excepciones llegaron a ser clases

El siguiente texto es una traducción del artículo How Exceptions Came to be Classes de Guido van Rossum publicado en http://python-history.blogspot.com/.

Cómo las excepciones llegaron a ser clases

Pronto supe que quería que Python utilice excepciones para el manejo de errores. Sin embargo, una parte crítica para que las excepciones funcionen es lograr algún tipo de esquema para identificar distintos tipos de excepciones. En los lenguajes modernos (incluyendo el moderno Python :-), las excepciones son definidas en términos de clases definidas por los usuarios. Sin embargo, en los comienzos de Python, elegí identificar las excepciones por strings. Esto fue desafortunado, pero tenía dos razones para tomar esta aproximación. Primero, aprendí sobre las excepciones en Modula-3, donde las mismas son señales únicas. Segundo, introduje las excepciones antes de introducir las clases definidas por los usuarios.

En teoría, supongo que podría haber creado un nuevo tipo de objeto a medida para ser utilizado en excepciones, pero como todo tipo de objeto a medida requiere un considerable esfuerzo de codificación en C, decidí reutilizar un tipo a medida existente. Y, ya que las excepciones están relacionadas con mensajes de error, pareció natural usar strings para representar excepciones.

Lamentablemente elegí una semántica donde diferentes objetos string podían representar diferentes excepciones, aún cuando tenían el mismo valor (es decir, contenían la misma secuencia de caracteres). Elegí esta semántica porque quería que las excepciones definidas en distintos módulos sean independientes, incluso si sucedía que tenían el mismo valor. La idea era que las excepciones siempre debían ser referenciadas por su nombre, lo que implicaría identidad del objeto, nunca por su valor, lo que requeriría string iguales.

Este enfoque fue influenciado por las excepciones de Modula-3, donde cada declaración de excepción crea una única “señal de excepción” que no puede ser confundida con cualquier otra. Creo que también quería optimizar el testeo de excepciones usando punteros de comparación en vez de valores de strings en un equivocado intento de optimización prematura del tiempo de ejecución (un caso inusual – ¡yo generalmente optimizaba mi propio tiempo de codificación!). La principal razón, sin embargo, es que me preocupaban los conflictos de nombres entre excepciones no relacionadas definidas en diferentes módulos. Intenté usar patrones para adherirme estrictamente a la convención de definir una excepción como una constante global en algunos módulos, y, entonces, usarla por nombre en todo, lanzamiento o captura (esto fue mucho tiempo antes de que ciertos string literales fueran automáticamente “internos”).

¡Ay de mí!, en la práctica las cosas nunca resultan como se espera. Pronto los usuarios de Python descubrieron que dentro del mismo módulo, el compilador byte code unificaba los strings literales (es decir, creaba un sólo objeto compartido para todas las ocurrencias de strings literales con el mismo valor). Así, por accidente, los usuarios encontraron que las excepciones podían ser capturadas especificando el nombre de la excepción o el string literal conteniendo el mensaje de error. Bien, al menos esto parecía trabajar la mayor parte del tiempo. En realidad, esto sólo trabajó para el código definido en el mismo módulo. Si uno intentaba capturar excepciones usando el mensaje de error de la excepción en un módulo diferente, el código se rompía misteriosamente. Es innecesario decir que este es el tipo de cosas que causa una gran confusión.

En 1997, con Python 1.5, introduje clases para excepciones dentro del lenguaje. Aunque este ha sido el enfoque recomendado desde entonces, las excepciones de strings todavía eran soportadas para el uso en determinadas aplicaciones heredadas hasta Python 2.5. Estas fueron eliminadas en Pyton 2.6.

Traducido por César Portela.

Revisado por Juan José Conti.

Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.


La historia de Python: Cómo todo se convirtió en sentencias ejecutables

El siguiente texto es una traducción del artículo How Everything Became an Executable Statement de Guido van Rossum publicado en http://python-history.blogspot.com/.

Cómo todo se convirtió en sentencias ejecutables

Los nuevos usuarios de Python a veces se sorprenden al descubrir que todas las partes del lenguaje son sentencias ejecutables, incluyendo la definición de funciones y clases. Eso significa que cualquier sentencia puede aparecer en cualquier lugar en un programa. Por ejemplo, una definición de una función puede aparecer dentro de una sentencia "if" si así se lo desea.

En una versión muy temprana de la gramática de Python esto no era así: los elementos de la gramática tenían un "sabor decorativo", las sentencias import y la definición de funciones solo eran permitidas en el nivel superior de un módulo o script (dónde eran ejecutadas para efectivizarse).

De todas formas, cuando estaba agregando soporte para clases, decidí que esto era muy restrictivo.

Mi razonamiento fue más o menos como sigue. En lugar de definir el cuerpo de una clase sólo como una serie de declaraciones de funciones, también parecía adecuado permitir asignaciones a variables allí. De todas formas, si iba a permitir eso, ¿por qué no ir un escalón más arriba y permitir código ejecutable arbitrario? O, llevando esto aún más lejos, ¿por qué no permitir declaración de funciones dentro de sentencias "if", por ejemplo? Rápidamente se vio que esto permitía una simplificación de la gramática, ya que ahora todos los usos de sentencias (estén identados o no) podían compartir la misma regla de gramática, y de hecho el compilador podría usar la misma función generadora de byte code para todas ellas.

A pesar de que este razonamiento me permitía simplificar la gramática y los usuarios podían colocar sentencias Python en cualquier lugar, esta característica no habilitaba necesariamente ciertos estilos de programación. Por ejemplo, la gramática de Python técnicamente permitía a los usuarios escribir cosas como funciones anidadas aunque la semántica subyacente de Python no aceptara ámbitos anidados. Por lo tanto, el código así operaría de formas inesperadas o "rotas" comparadas con lenguajes que realmente estaban diseñados con esa característica en mente. Con el paso del tiempo, muchas de esas características "rotas" se arreglaron. Por ejemplo, la definición de funciones anidadas sólo empezó a funcionar un poco más correcta en Python 2.1.

Traducido por Juan José Conti.

Revisado por César Portela.

Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.


La historia de Python: Todo de primera clase

El siguiente texto es una traducción del artículo First-class Everything de Guido van Rossum publicado en http://python-history.blogspot.com/.

Todo de primera clase

Uno de mis objetivos para Python era hacerlo de tal forma que todos los objetos sean de "primera clase". Con esto me refiero a que quería que todos los objetos puedan ser nombrados en el lenguaje (por ejemplo, enteros, strings, funciones, clases, módulos, métodos, etc.) para tener igual status. Entonces pueden ser asignados a variables, ubicados en listas, almacenados en diccionarios, pasados como argumentos y más.

La implementación interna de Python hizo que esto sea fácil de hacer. Todos los objetos de Python estaban basados en una estructura de datos de C común que se usaba en todos los lugares del intérprete. Variables, listas, funciones y todo lo demás usaba variaciones de esta estructura de datos; directamente no importaba si la estructura representaba un objeto simple como un entero o algo más complicado como una clase.

Aunque la idea de tener "todo de primera clase" es conceptualmente simple, había aún un aspecto de las clases que necesitaba resolver; el problema de hacer que los métodos sean objetos de primera clase.

Consideremos esta clase simple en Python (copiada de la entrada de la semana pasada):

class A:

    def __init__(self,x):

        self.x = x

    def spam(self,y):

        print self.x, y

Si los métodos van a ser objetos de primera clase, entonces pueden ser asignados a otras variables y usados como cualquier otro objeto en Python. Por ejemplo, alguien podría escribir una sentencia en Python como "s = A.spam". En este caso la variable "s" referencia un método de una clase, que en realidad es solo una función. Sin embargo, un método no es exactamente igual a una función. En concreto, se supone que el primer argumento de un método es una instancia de la clase en la que el método fue definido.

Para tratar esto cree un tipo de objeto invocable (callable) conocido como "unbound method". Un unbound method era en realidad un wrapper delgado alrededor de un objeto función que implementaba un método, pero forzaba la restricción de que el primer argumento tenía que ser una instancia de la clase en la cual el método fue definido. Así, si alguien quería llamar al unbound method "s" como una función, tendrían que pasar una instancia de la clase "A" como primer argumento. Por ejemplo, "a = A(); s(a)".(*)

Un problema relacionado ocurre si alguien escribe una sentencia Python que refiere al método en una instancia específica de un objeto. Por ejemplo, alguien puede crear una instancia usando "a = A()" y luego escribir una sentencia como "s = a.spam". Aquí la variable "s" nuevamente referencia al método de una clase, pero la referencia a ese método se obtuvo a través de la instancia "a". Para manejar esta situación se usa un objeto invocable diferente llamado "bound method". Este objeto es también un wrapper delgado alrededor del objeto función para el método. Sin embargo, este envoltorio implícitamente almacena la instancia original que fue usada para obtener el método. Así, una sentencia futura como "s()" llamará al método implícitamente con la instancia "a" como el primer argumento.

En realidad el mismo objeto interno es usado para representar los bound y unbound methods. Uno de los atributos de este objeto contiene una referencia a una instancia. Si es None, el método es unbound. De otro modo, el método es bound.

A pesar de que bound y unbound methods parezcan un detalle sin importancia, son una parte crítica de como las clases funcionan bajo el tapete. Siempre que una sentencia como "a.spam()" aparece en un programa, la ejecución de la sentencia ocurre en dos partes. Primero ocurre la búsqueda de "a.spam". Esto retorna un bound method; un objeto invocable. Luego, una operación de llamado de función "()" es aplicada a ese objeto para invocar el método con los argumentos provistos por el usuario.

(*) En Python 3000, el concepto de unbound methods se eliminó y la expresión "A.spam" retorna un objeto función normal. Nos dimos cuenta de que la restricción de que el primer argumento sea una instancia de A ayudaba pocas veces al diagnosticar problemas y frecuentemente era un obstáculo para usos avanzados; alguien lo llamó "duck typing self", el cual parece un nombre apropiado.

Traducido por Juan José Conti.

Revisado por César Portela.

Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.


La historia de Python: Clases definidas por los usuarios

El siguiente texto es una traducción del artículo Adding Support for User-defined Classes de Guido van Rossum publicado en http://python-history.blogspot.com/.

Añadir clases definidas por los usuarios

Crease o no, las clases fueron un añadido tardío durante el primer año del desarrollo de Python, todavía en el CWI, aunque bastante antes de la primera versión pública. En cualquier caso, para entender como se añadieron las clases, ayuda saber un poco más sobre los detalles de implementación de Python.

Python está escrito en C en forma de un intérprete de código intermedio o pseudo-binario (bytecode), usando la clásica estructura de pila, junto con una colección de tipos primitivos, también implementados en C. La arquitectura subyacente usa "objetos", pero como C no soporta objetos directamente, se implementan usando estructuras de objetos y punteros a funciones. La máquina virtual Python define docenas de operaciones estándar que cada objeto debe o puede implementar (por ejemplo, get_attribute, add y call).

Un objeto se representa mediante una estructura estática que contiene una serie de punteros a funciones, uno para cada operación estándar. Estos punteros son inicializados normalmente con referencias a funciones estáticas. Pero algunas operaciones son opcionales y un objeto puede dejar esas entradas apuntando a NULL si decide no implementar la función. En este caso, la máquina virtual o bien genera un error en tiempo de ejecución o, en determinadas circunstancias, puede que proporcione una implementación por defecto de la operación. La estructura C contiene también varios campos de datos, uno de los cuales es una referencia a la lista de métodos adicionales que son únicos para ese tipo de datos, representada como una matriz de estructuras que constan de un texto (el nombre del método) y un puntero a una función (la implementación). El enfoque a la introspección de

Python deriva de esta habilidad de hacer que la propia estructura del tipo sea accesible en tiempo de ejecución, como cualquier otro objeto.

Un aspecto importante de esta implementación es que está completamente centrada en el lenguaje C. De hecho, todas las operaciones y los métodos estándar están implementados por funciones en C. En un principio, el interprete de bytecode solo soportaba llamadas a funciones escritas en Python puro y funciones o métodos implementados en C. Creo que fue mi colega Siebren van der Zee el primero en sugerir que Python debería permitir definiciones de clases similares a las de C++, que permitieran al programador crear objetos propios.

Para poder implementar estos objetos de usuario, me ceñí al diseño más simple que pude imaginar: un esquema donde los objetos de usuario se representarían por nuevos objetos que almacenarían una referencia de clase que apuntaría a un "objeto clase" compartido por todas las instancias de la misma clase, y un diccionario, bautizado "diccionario de instancia", que contendría las variables particulares de cada instancia.

En esta implementación, el diccionario de la instancia contendría los valores de las variables de cada instancia, mientras que el objeto clase contendría la información que fuera compartida entre todas las instancias de la misma clase, especialmente, los métodos. Al implementar la clase objeto opté de nuevo por el diseño más sencillo posible; el conjunto de métodos de la clase se almacenaría en un diccionario, cuyas claves serían los nombres de los métodos, con lo que se creó el diccionario de la clase. Para implementar la herencia, los objetos clase almacenarían opcionalmente una referencia a los objetos clase correspondientes a las clases base. En esa época era bastante ingenuo en lo que se refería a las clases, pero sabía que existía la herencia múltiple, que C++ había incorporado recientemente. Decidí que si iba a implementar la herencia, bien podría implementar una versión simplificada de la herencia múltiple, de forma que una clase pudiera derivar de más de una clase base.

En esta implementación, los mecanismos subyacentes que gestionaban los objetos eran en realidad muy simples. Cualquier cambio hecho a las variables, ya sea de clase o de instancia, se verían reflejados en el objeto diccionario respectivo.

Por ejemplo, asignar un valor a una variable de una instancia actualizaría su diccionario local. De igual forma, cuando buscáramos el valor de una variable de instancia de un objeto, simplemente miramos en el diccionario subyacente. Si la variable no se encuentra allí, las cosas se ponen un poco más interesantes. En ese caso, las búsquedas deben realizarse en el diccionario asociado a la clase, y si tampoco se encontrara allí, en los diccionarios de cada clase de la que derive.

Es más habitual ver este mecanismo de búsqueda de atributos en la clase del objeto, así como en sus clases antecesoras, en el caso de la búsqueda de métodos. Como se ha mencionado anteriormente, los métodos se almacenan en el diccionario de la clase, por lo que son compartidos por todas las instancias de objetos pertenecientes a dicha clase. Así, cuando se invoca un método, lo normal es que no lo encuentres en el diccionario local del objeto. En vez de eso, se busca el método en la clase del objeto, y de no encontrarse, su busca sistemáticamente por todas las clases de las que deriva hasta encontrarlo. Cada una de las clases básicas implementa el mismo algoritmo recursivo. Esto se conoce habitualmente como la regla de primero en profundidad, luego de derecha a izquierda, y ha sido el método de ordenación y selección de métodos (MRO - Method Resolution Order) usado por Python en la mayoría de sus versiones.

Las versiones más modernas han adoptado un MRO más sofisticado, que se discutirá en un futuro artículo de esta serie.

Al implementar las clases, uno de mis objetivos fue mantener las cosas sencillas. Así, Python no realiza comprobaciones de errores ni comprueba inconsistencias a la hora de localizar métodos. Por ejemplo, si una clase sobreescribe un método definido en una clase antecesora, no se realiza ninguna comprobación para verificar que el método redefinido tenga el mismo número de argumentos, ni que puede ser llamada de la misma manera que el método original. El algoritmo de resolución y localización de métodos se limita a devolver el primer método que encuentre, y lo ejecuta con cualesquiera argumentos que haya indicado el usuario.

A partir de este diseño emergieron otras características. Por ejemplo, aunque el diccionario de clase se pensó inicialmente como un repositorio de métodos, no existía ninguna razón que le impidiera contener también otros tipos de objetos.

Así, objetos como números enteros o cadenas de texto podían ser almacenados en el diccionario de la clase, lo que los convertía a todos los efectos en variables de clase; variables que son compartidas por todas las instancias de una determinada clase, en vez de estar almacenadas localmente.

Aunque la implementación era sencilla, también proporcionaba un alto grado de flexibilidad. Por ejemplo, la implementación hacía que las propias clases fueran objetos, en pie de igualdad con cualquier otro objeto (objetos de primera clase, o first-class objects, como se les suele describir en la documentación), lo que significaba que podían ser inspeccionadas de forma introspectiva en tiempo de ejecución, e incluso ser modificadas inámicamente. Se podían añadir o modificar métodos simplemente actualizando el diccionario de la clase, una vez que la clase hubiera sido creada (*). La naturaleza dinámica de Python significaba que esos cambios tendrían un efecto inmediato en todas las instancias de esa clase o de sus clases derivadas. De igual manera, se podía modificar dinámicamente objetos individuales añadiendo, modificando o borrando variables de instancia (una característica que, como comprendí posteriormente, hacía que la implementación de clases y objetos de Python fuera más permisiva que la de Smalltalk, que restringía el conjunto de atributos a aquellos especificados en el momento de la creación).

Desarrollo de la sintaxis de clases

Habiendo diseñado las representaciones en tiempo de ejecución para las clases definidas por el usuario, mi siguiente tarea era diseñar la sintaxis para las definiciones de clases, y en particular, para las definiciones de métodos dentro de la clase. Había una restricción fuerte y era que yo no quería que la sintaxis para definir métodos fuera distinta de la sintaxis para definir funciones.

Reconstruir la gramática y el generador de bytecode para manejar estos dos casos tan similares de forma diferente fue una tarea ardua. Aun así, aunque conseguí mantener la gramática igual, aún tenía que encontrar la manera de tratar con las variables de instancia. Inicialmente había esperado emular las variables de instancia implícitas que podemos ver, por ejemplo, en C++. En ese lenguaje, las clases se definen con un código como el siguiente:


    class A {

    public:

       int x;

       void spam(int y) {

            printf("%d %d\n", x, y);

       }

    };

En esta clase se ha declarado la variable de instancia x. En los métodos, las referencias a x se refieren implícitamente a la variable de instancia.

Por ejemplo, en el método spam(), no se declara la variable x ni como parámetro, ni como variable local, pero como la clase ha declarado una variable de instancia del mismo nombre, se asume que las referencias a x se refieren a dicha variable. Aunque deseaba proporcionar a Python algo similar, pronto me di cuenta de que esta aproximación sería imposible, ya que, en un lenguaje que carece de declaración de variables, no habría una manera elegante de distinguir las variables de instancia de las variables locales.

En teoría, obtener el valor de las variables de instancia debería ser bastante fácil. Python ya disponía de un orden de búsqueda predefinido para nombres de variables no cualificados: locales, globales e internas (built-ins).

Cada una de estas áreas estaba representada por un diccionario que mapeaba los nombres de las variables con sus valores. Cada referencia a una variable se convertía, así, en una serie de búsquedas en diccionarios que concluía cuando se encontrada el nombre de la variable. Por ejemplo, durante la ejecución de una función con una variable local p y una variable global q, en una sentencia como, por ejemplo, print p, q buscaría p en el primer diccionario, el de las variables locales, y lo encontraría. Luego buscaría q en ese mismo diccionario y no lo encontraría, por lo que continuaría la búsqueda por el segundo diccionario, el de las variables globales, hasta encontrarlo.

Habría sido muy fácil añadir el diccionario de instancia del objeto actual al principio de esta lista de diccionarios a la hora de ejecutar un método. De esa forma, en un método de un objeto con una variable de instancia x y una variable local y, una sentencia como print x,y encontraría x en el diccionario de la instancia (el primer diccionario según la nueva ordenación), e y en el diccionario de variables locales (el segundo

diccionario).

El problema con esta estrategia es que fracasa al intentar declarar los valores de las variables de instancia. La asignación en Python no busca el nombre de la variable en los diccionarios, sino que se limita a añadir o reemplazar la variable en el primer diccionario de la lista, normalmente el de variables locales. Esto provoca que las variables siempre se creen en el ámbito local, si no se especifica nada (aunque hay que hacer notar que existe una “declaración global" que invalida este comportamiento para una variable dentro de una función).

Si no cambiamos esta aproximación minimalista a la asignación, el que el diccionario de la instancia fuera el primero en la lista de búsqueda haría

imposible asignar valores a las variables locales dentro de un método. Porejemplo, si tuviéramos un método así:


    def spam(y):

        x = 1       

        y = 2       

Las asignaciones a x e y sobreescribirían el valor de la variable de instacia x y crearían una nueva variable de instancia y, que impediría acceder al valor de la variable local y. Cambiar el orden de los diccionarios (pasar el de instacia al segundo lugar y que el diccionario

local se convirtiera en el primero) simplemente la daría la vuelta al problema, haciendo imposible realizar asignaciones a variables de instancia.

Tampoco funcionaría cambiar la semántica de las asignaciones para usar una variable de instancia, si existe alguna, o usar una variable local en caso contrario, porque esto nos crearía un problema de auto-referencias: ¿cómo crearíamos una variable de instancia, en primer lugar? Una posible solución podría ser obligar a declarar explícitamente las variables de instancia, de forma similar a la usada para declarar variables globales, pero no quería añadir una característica como esta, habiendo llegado tan lejos como había llegado sin requerir ninguna declaración de variables. Además, la especificación extra para indicar una variable global era un caso especial que apenas se usaba en la mayoría del código. La declaración explícita de variables de instancia, por otro lado, tendría que ser usada en prácticamente cualquier definición de clase. Otra posible solución era distinguir lexicamente las variables de instancia. Por ejemplo, usando un símbolo especial como el caracter @ (una aproximación tomada por ruby) o usando alguna convención de nombres que implicara prefijos o un uso particular de mayúsculas y minúsculas. Ninguna de estas opciones me agradaba (y sigue sin hacerlo).

En vez de esto, decidí abandonar la idea de referencias implícitas a las variables de instancia. Los lenguajes como C++ permiten escribir cosas como this->foo, para señalar explícitamente que la variable foo es de instancia, distinguiéndola así de una posible variable local foo. Decidí,

por tanto, hacer que la única manera de acceder a las variables de instancia fueran estas referencias explícitas. Además, tomé la decisión de que this, la variable que representaba al objeto actual, no fuera una palabra clave, simplemente haría que this (o su equivalente) fuera un primer argumento de cada método. Las variables de instancia sería siempre atributos de ese argumento.

Usando referencias explícitas, no había ninguna necesidad de tener una sintaxis especial para la definición de métodos, ni tenía uno que complicarse con semánticas adicionales para la búsqueda de variables. En vez de eso, simplemente se definía una función, sabiendo que el primer argumento correspondería con el objeto instanciado. Por convención, se suele dar a este primer argumento el nombre de self. Por ejemplo:


    def spam(self,y):

        print self.x, y

Esta aproximación recuerda algo a Modula-3, que ya me había proporcionado la sintaxis para las importaciones y para el manejo de excepciones. Modula-3 no tenía clases, pero permitía definir tipos estructurados que podían contener punteros a funciones, que eran inicializadas por defecto con funciones definidas previamente y añadía azúcar sintáctico para que, si x era una estructura de ese tipo y m un puntero a una función almacenada en dicho registro, inicializado a una función f, entonces llamar a x.m(args) equivalía a llamar a f(x, args). Esto se ajusta a la implementación de objetos y métodos, y hace posible equiparar las variables de instancia con atributos del primer argumento.

El resto de los detalles de la sintaxis de Python para clases se derivan de este diseño o de las demás restricciones impuestas por la implementación. Siguiendo con mis aspiraciones de sencillez, imaginaba la sentencia class como una serie de definiciones de métodos, que son sintácticamente iguales a las definiciones de funciones, aun cuando se estableciera por convención que todas deberían tener un primer argumento llamado self. Además, en vez de desarrollar una nueva sintaxis para los métodos especiales (como los constructores y los destructores), tomé la decisión de que estos casos se resolverían obligando al usuario a utilizar nombres especiales, como init, del y demás. Esta convención de nombres se tomó del lenguaje C, en el que los identificadores que empezaban con el caracter guión bajo estaban reservados para el compilador y tenían, a menudo, significados especiales (por ejemplo, macros como FILE en el preprocesador de C).

Así, la visión que tenía del código para definir una clase era esta:


    class A:

         def __init__(self,x):

             self.x = x

         def spam(self,y):

            print self.x, y




También quería seguir reutilizando la máxima cantidad posible de código.

Normalmente, una definición de una función es una sentencia ejecutable que, simplemente, realiza una asignación; asigna a una variable, en el espacio de nombres local, el objeto función (el nombre de la variable será, por tanto, el nombre de la función). Se me ocurrió que, en vez de inventar una solución distinta, era razonable hacer la misma interpretación para las definiciones de métodos dentro del cuerpo de la clase, simplemente usando como espacio de

nombres un nuevo diccionario. Este nuevo diccionario sería entonces tratado y usado para inicializar el diccionario de la clase, creando de esa forma una nueva clase. Detrás de escena, la estrategia que se implementó fue convertir el cuerpo de la clase en una función anónima, que ejecutaba todas las sentencias de definición de métodos que encontrara en el cuerpo de la clase, y que terminaba devolviendo un diccionario con todas las variables/métodos definidas. Este diccionario se pasaba a una función auxiliar, que creaba la clase en sí. Finalmente, el objeto que definía la propia clase se almacenaba en una variable en el entorno local, siendo su nombre el mismo que el de la clase.

Los usuarios de Python a menudo se sorprenden al comprender que cualquier sentencia válida de Python puede aparecer en el cuerpo de una clase. Esta característica era en realidad una extensión de mi deseo de mantener la sintaxis lo más limpia posible, a la vez que trataba de no limitar artificialmente aquellas cosas que pudieran resultar útiles.

Un detalle final acerca de la sintaxis usada para instanciar objetos de una clase. Otros lenguajes, como C++ o Java, usan para crear objetos un operador

especial, new. En C++ esta opción es defendible, porque los nombres de las clases tienen un estatus especial para el analizador, pero en Python eso no era así. Como el analizador de Python no se preocupa en absoluto por el tipo de objeto que esta llamando, hacer que la propia clase fuera ejecutable era la solución correcta, "mínima" en el sentido de que no requería una nueva sintaxis.

Creo que me adelanté un poco a los tiempos aquí; a día de hoy, el “patrón de diseño Factory” es a menudo el sistema más empleado para la creación de instancias y lo que yo hice fue simplemente convertir cada clase en su propia fábrica (Factory).

Métodos especiales

Como decía en la última sección, uno de los objetivos que perseguía era que la implementación de las clases fuera sencilla. En los demás lenguajes orientados a objetos, normalmente existe una diversidad de métodos y operadores especiales que sólo se aplican a las clases. Por ejemplo, en C++, hay una sintaxis especial para definir constructores y destructores, diferente de la usada para definir funciones o métodos normales.

En realidad, no quería introducir una nueva sintaxis para manejar las operaciones especiales con los objetos. Así que me las arreglé para mapear los

operadores específicos con un conjunto de nombres especiales de métodos, como init y del. Los usuarios podrían definir su propio código asociado a la creación y destrucción de objetos, simplemente definiendo métodos con estos nombres especiales.

Usé la misma técnica para permitir a los usuarios redefinir el comportamiento de los operadores de Python. Como ya se ha dicho, Python está escrito en C y usa tablas que contienen punteros a funciones para implementar diferentes capacidades de los objetos internos (por ejemplo, get attribute, add y call). Para permitir que el usuario pudiera definir estas mismas capacidades en sus clases, mapeé los punteros a diferentes funciones con nombres especiales como getattr, add y call.

Existe una correspondencia directa entre estos nombres y las tablas de punteros de funciones que uno tiene que definir cuando se implemente un nuevo tipo de objeto en C.

(*) Eventualmente, el nuevo estilo de clases hace que sea necesario controlar los cambios en el dict de la clase; aún se puede modificar dinámicamente las clases, pero se debe utilizar asignación de atributos en lugar de la variable dict directamente.

Traducido por Juan I. Rodriguez.

Revisado por Juan José Conti y César Portela.

Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.


La historia de Python: El uso de tipado dinámico

El siguiente texto es una traducción del artículo Python's Use of Dynamic Typing de Guido van Rossum publicado en http://python-history.blogspot.com/.

El uso de tipado dinámico en Python

Una diferencia importante entre ABC y Python es el estilo general del sistema de tipos. ABC es estáticamente tipado, lo cual significa que el compilador de ABC analiza el uso de tipos en un programa y decide si están siendo usados consistentemente. Si no, el programa es rechazado y su ejecución no puede comenzar. A diferencia de la mayoría de los lenguajes con tipado estático de esos días, ABC usaba inferencia de tipos (no distinto que Haskell) en lugar de declaraciones explícitas de tipos como en C. En contraste, Python es dinámicamente tipado. El compilador de Python ignora felizmente los tipos usados en un programa y todo el control de tipos es hecho en tiempo de ejecución.

Aunque esto pueda parecer muy distinto de ABC, no es tan diferente como uno imaginaría. A diferencia de otros lenguajes de tipado estático, ABC no depende (¿dependía? es prácticamente histórico hoy : - ) exclusivamente de controles de tipado estático para evitar que el programa termine abruptamente, también tiene una librería en tiempo de ejecución que controla los tipos de los argumentos en todas las operaciones nuevamente cada vez que son ejecutadas. Esta verificación no estaba de más para los algoritmos de control de tipos del compilador, que no estaban totalmente implementados en el primer prototipo del lenguaje. La librería en tiempo de ejecución también servía como una ayuda para la depuración, ya que el control de tipos explícito en tiempo de ejecución puede producir lindos mensajes de error (algo requerido por los implementadores), en lugar de los vuelcos de memoria que sucederían si el intérprete siguiera ciegamente con una operación sin controlar si los argumentos tienen sentido.

Sin embargo, la razón más importante por la que ABC tenía control de tipos en tiempo de ejecución, además de control de tipado estático, es su naturaleza interactiva. En una sesión interactiva, el usuario tipea sentencias de ABC y definiciones que son ejecutadas tan pronto como son completadas. En una sesión interactiva, es posible crear una variable y asignarle un número, borrarla y luego volver a crearla (en otras palabras, crear otra variable con el mismo nombre) y asignarle un string. Dentro de un solo procedimiento, sería un error de tipado estático usar el mismo nombre de variable primero como un número y luego como un string, pero no sería razonable forzar ese control entre diferentes sentencias entradas en una sesión interactiva, mientras que la creación accidental de una variable llamada x a la que se le asigna un número, !prohiba para siempre la creación de una variable x con otro tipo!. El compromiso de ABC es usar control de tipos dinámico para las variables globales, pero estático para las locales. Para simplificar la implementación, las variables locales obtienen control de tipo dinámico también.

Así, hay solo un pequeño paso desde el enfoque usado en la implementación de ABC para el control de tipos al de Python; Python simplemente deja todo el control de tipos en tiempo de compilación. Esto se alinea completamente con la filosofía de Python de "tomar atajos", ya que simplifica la implementación y no afecta la eventual seguridad, ya que todos los errores de tipo son atrapados en tiempo de ejecución antes de que causen un mal funcionamiento del intérprete de Python.

Sin embargo, una vez que te decides por el tipado dinámico no hay vuelta atrás. Las operaciones de ABC fueron cuidadosamente diseñadas para que el tipo de los argumentos pueda ser deducido de la forma de los operadores. Por ejemplo, de la expresión "x^y" el compilador deduciría que las variables x e y son strings, así como el resultado. En Python, esa deducción no se puede generalizar. Por ejemplo, la expresión "x+y" puede ser una concatenación de strings, una suma entre números, o una operación sobrecargada sobre tipos definidos por el usuarios.

Traducido por Juan José Conti.

Revisado por César Portela.

Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.


La historia de Python: El principio del diseño y desarrollo del lenguaje

El siguiente texto es una traducción del artículo Early Language Design and Development Guido van Rossum publicado en http://python-history.blogspot.com/.

El principio del diseño y desarrollo del lenguaje

De ABC a Python

La primera y principal influencia de Python fue ABC, un lenguaje diseñado a principios de los 80 por Lamber Meertens, Leo Geurts y otros en CWI. El objetivo de ABC era ser un lenguaje de enseñanza, un reemplazo para BASIC, y un lenguaje y entorno para computación personal. Fue diseñado en un principio haciendo un análisis de la tarea de programar y luego haciendo varias iteraciones que incluían pruebas de usuario a conciencia. Mi rol en el grupo de ABC era principalmente implementar el lenguaje y su entorno integrado de edición.

El uso que Python hace de la identación viene directamente de ABC, pero esta idea no se originó con ABC (ya había sido promovida por Donald Knuth y era un concepto bien conocido de estilo de programación). (El lenguaje de programación occam también lo usaba). Si embargo, los autores de ABC sí inventaron el uso de los dos puntos que separa la cláusula inicial del bloque identado. Luego de las primeras pruebas con usuarios sin los dos puntos, se descubrió que el significado de la identación no le quedaba claro a los principiantes que tomaban sus primeras lecciones de programación. Agregar los dos puntos clarificó su significado: los dos puntos de alguna formar guiaban la atención a lo que seguía y unía lo anterior con lo siguiente de forma correcta.

Los principales tipos de datos de Python también vienen de ABC, aunque con algunas modificaciones. Las listas en ABC eran en realidad bags o multisets, que siempre se mantenían ordenadas utilizando una implementación modificada de árboles B. Sus tablas eran arrays asociativos que se mantenían ordenados en forma similar mediante claves. Encontré que ningún tipo de dato era preciso para representar, por ejemplo, la secuencia de líneas leídas de un archivo, el cual anticipé que sería un caso de uso común (en ABC tenías que usar una tabla con el número de línea como clave, pero eso complicaba las inserciones y los borrados). Entonces convertí el tipo lista en un array flexible con operaciones de inserción y borrado, dándole a los usuarios control total sobre el orden de los elementos en una lista. Un método sort soportaba la necesidad ocasional de resultados ordenados.

También reemplacé las tablas ordenadas implementando una tabla hash. Elegí una tabla hash porque creía que sería más rápida y fácil de implementar que el árbol B de ABC. Estaba teóricamente probado que los árboles B eran asintóticamente óptimos en tiempo y espacio para una gran variedad de operaciones, pero en la práctica se volvieron difíciles de implementar correctamente debido a la complejidad de sus algoritmos. Por la misma razón, la performance tampoco era óptima para tablas pequeñas.

Mantuve el tipo de dato inmutable de ABC llamado tupla (las operaciones de empaquetado y desempaquetado en Python vienen directamente de ABC). Ya que las tuplas son implementadas mediante arrays, decidí agregarles indexación y rebanado.

Una consecuencia de añadirle una interfaz de tipo array a las tuplas fue que tuve que pensar en una forma de resolver los casos límites de tuplas de longitud 0 ó 1. Una de las reglas que tomé de ABC fue que cada tipo de datos, al ser impreso o convertido a string, debía ser representado por una expresión que sea una entrada válida para el parser del lenguaje. De esto siguió que necesitaba notaciones para las tuplas de longitud 0 y 1. Al mismo tiempo no quería perder la distinción entre una tupla y una expresión entre paréntesis, entonces utilicé un enfoque feo pero pragmático en el cual una coma final convertiría una expresión en una tupla de un elemento y "()" representaría a una tupla de cero elementos. Vale la pena mencionar que los paréntesis por lo general no son necesarios en la sintaxis de Python, excepto aquí (representar la tupla vacía con "nada" podría fácilmente enmascarar errores genuinos).

Los strings de Python empezaron con una semántica (inmutable) muy parecida a los strings de ABC, pero con una notación diferente e indexación basada en 0. Ya que ahora tenía tres tipos indexables -listas, tuplas y strings- decidí generalizar todo en un concepto común, la secuencia. Esta generalización hizo que ciertas operaciones básicas como obtener la longitud (len(s)), indexar (s[i]), rebanar (s[i:j]) e iterar (for i in s) funcionen de la misma forma en cualquier tipo que sea una secuencia.

Los números son uno de los puntos en los que más en desacuerdo estuve con ABC. ABC tenía dos tipos de números en tiempo de ejecución; los números exactos que eran representados como números racionales de precisión arbitraria y los números aproximados que eran representados mediante punto flotante binario con un rango de exponente extendido. Los números racionales no encajaban en mi visión del tema (anécdota: una vez intenté computar mis impuestos usando ABC. El programa, que parecía bastante directo, estaba demorando mucho en computar unos pocos números. Luego de investigar descubrí que estaba haciendo aritmética con números con miles de dígitos de precisión, que tenían que ser redondeados a florines -pie 100 centavos holandeses - y centavos para ser impresos). Es por esto que para Python elegí un modelo más tradicional con enteros de máquina y punto flotante binario de máquina. En la implementación de Python, estos números son representados simplemente con los tipos de datos de C long y double respectivamente.

Creyendo que también había un caso de uso importante para números exactos sin límite, agregué un tipo de dato bignum, que llamé long. Ya tenía una implementación de bignum que había sido el resultado de un intento inconcluso por mejorar la implementación de ABC unos años antes (la implementación original de ABC, una de mis primeras contribuciones, usaba una representación decimal internamente). Sonaba lógico usar este código en Python.

A pesar de haber agregado bignums a Python, es importante enfatizar que no quería usar bignums para todas las operaciones entre enteros. De extrapolar lo que veía en programas escritos por mí y por colegas en CWI, sabía que las operaciones entre enteros representaban una porción significativa del total del tiempo que la mayoría de los programas corrían. El uso más común de los enteros es indexar secuencias que entran en memoria. Así, decidí usar enteros de máquina para los casos de uso más comunes y el rango extra de bignums solo para hacer "matemática seria" o calcular la deuda externa de Estados Unidos en peniques.

El problema con los números

La implementación de números, especialmente enteros, es un área en la que cometí varios errores de diseño serios, pero también aprendí lecciones importantes sobre el diseño de Python.

Ya que Python tiene dos tipos diferentes de enteros, necesitaba una forma de distinguir entre los dos tipos en un programa. Mi solución era pedirle a los usuarios que explícitamente digan cuando querían usarlos agregando una L al final de los números (por ejemplo 1234L). Esta es un área en la que Python violaba la filosofía inspirada en ABC de no necesitar que los usuarios se encargar de detalles de implementación que no les importaban.

Lamentablemente, este era solo el menor detalle de un problema mayor. Un error más ilustre fue que mi implementación de enteros y longs ¡tenía una ligera diferencia semántica en algunos casos! Ya que el tipo int era representado como un entero de máquina, las operaciones que desbordaban silenciosamente recortaban el resultado a 32 bits o a la precisión que el tipo long de C tuviera. Además, el tipo int, que normalmente se considera tiene signo, era tratado como sin signo por las operaciones bitwise y shift y en la conversión desde/hacia octales o hexadecimales representados como int o long. Los longs, por otro lado, siempre se consideraban con signo. Por lo tanto, algunas operaciones producían un resultado diferente, dependiendo de si un argumento era representado como int o como long. Por ejemplo, en una aritmética de 32 bits, 1<<31 (1 shift a izquierda 31 bits) produciría el entero negativo más grande de 32 bits y 1<<32 produciría cero, mientras que 1L<<31 (1 representado como long shift a izquierda 31 bits) produciría un entero enorme igual a 231 y 1L<<32 produciría 232.

Para resolver algunos de estos asuntos hice un arreglo simple. En lugar de tener operaciones entre enteros que recorten silenciosamente el resultado, cambié la mayoría de las operaciones aritméticas para que lancen una excepción OverflowError cuando el resultado no encaje. (La única excepción a este control eran las operaciones de "bit-wise" mencionadas anteriormente, ya que asumí que los usuarios esperarían que estas operaciones se comporten como en C). Si no hubiese añadido este control, los usuarios de Python indudablemente hubiesen empezado a escribir código dependiente de la semántica de la aritmética binaria con signo de módulo 2**32 (como hacen los usuarios de C), y arreglar el error hubiese sido una transición mucho más dolorosa para la comunidad.

A pesar de que la inclusión del control de desborde pueda parecer un detalle de implementación menor, una dolorosa experiencia de debugging me hizo dar cuenta que era una característica útil. Como uno de mis primeros experimentos en Python, intenté implementar un algoritmo matemático simple, el computo de los "Números de Meertens", un poco de matemática recreativa inventada por Richard Bird al celebrar los 25 añosen WCI del principal autor de ABC. Los primeros números de Meertens son pequeños, pero al traducir el algoritmo en código no me había dado cuenta de que los resultados intermedios del computo eran mucho más grandes que 32 bits. Me llevó una larga y dolorosa sesión de debugging descubrir esto, y decidí entonces manejar el asunto controlando todas las operaciones entre enteros y lanzando una excepción siempre que el resultado no pueda ser representado como un long de C. El costo extra del control de desborde no se notaría junto a la sobrecarga que ya tenía con la decisión de implementación de crear un nuevo objeto para el resultado.

Lamentablemente, siento decir que lanzar una excepción por desborde ¡tampoco era la solución correcta! En ese entonces, estaba trabado por la regla de C "las operaciones con tipos numéricos T retornan un resultado de tipo T". Esta regla también era la razón de mi otro gran error en la semántica de los enteros: truncar el resultado de la división entre enteros, que discutiré en una de las próximas entradas. En retrospectiva, debí hacer que las operaciones entre enteros que desbordaban cambien el tipo de su resultado a long. Esta es la forma en que Python funciona hoy, pero completar esta transición llevó mucho tiempo.

A pesar del problema con los números, una cosa muy positiva salió de esta experiencia. Decidí que no debía haber valores de retorno no definidos en Python, en lugar de esto, siempre se lanzarían excepciones cuando un valor de retorno no correcto podía ser computado. Así, los programas escritos en Python nunca fallarían debido a que valores no definidos se estén pasando silenciosamente por detrás. Este es aún un principio importante del lenguaje, tanto en el lenguaje propiamente dicho como en las librerías.

Traducido por Juan José Conti.

Revisado por César Portela.

Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.