lunes, 13 de junio de 2011

La Estética de Python

Hasta ahora, las pocas entradas de este blog han versado sobre aspectos técnicos de la programación en Python.  No es la idea de este blog orientarlo  exclusivamente a estos aspectos, pues sería muy árido.   Se podría decir que por esto he intercalado, casi de contrabando, algunas pinturas surrealistas aquí y allá, aunque al hacerlo, tampoco me he salido del aspecto técnico, pues el arte es en gran parte un asunto de técnica también y la programación es un arte.  Hay matemáticos que afirman que la buena matemática lo es también en un sentido estético- lo verdadero y trascendental tiene que ser hermoso.  Esto vale también para la programación, que también es una actividad matemática, si se quiere.   El buen código tiene que ser estéticamente agradable - esta es la premisa fundamental de Python.

  Mond Guter Dinge
Max Ernst

El código Python se caracteriza por la indentación- el uso de espacio en blanco al comienzo de una línea para indicar el nivel, bloque o estructura de control al cual corresponde esa línea.  Yo he usado la indentación en el código prácticamente desde que di mis primeros pasos con la programación estructurada en PASCAL.  Como ustedes sabrán, este lenguaje no obliga el programador a indentar, pero afortunadamente para mi, el libro con el cual aprendí PASCAL usaba esta convención de estilo.  Pronto me di cuenta de que esto era algo bueno porque el código era mucho más legible que el código en BASIC, lenguaje este con el que aprendí a programar.

En lenguajes más antiguos como FORTRAN, el espacio en blanco era importante para demarcar ciertas partes de una línea de código.  Concretamente, las primeras 5 columnas de la línea indicaban el número de línea (opcional), la sexta columna contenía una "C" si la línea era un comentario, etc., aunque no me acuerdo si era exactamente así y me da flojera buscarlo en google para verificar...  En el proceso de evolución de los lenguajes de programación, esta práctica fué abandonada a favor del uso de ciertas palabras reservadas para indicar el comienzo y el fin de un bloque de código en un programa (el begin y end en PASCAL).   Con el tiempo, lenguajes como C abreviaron esto aún más mediante el uso de braces (los signos de corchetes: {}).  De modo que algunos programadores consideraban el uso de la indentación de Python como un retroceso en la evolución de los lenguajes cuando este lenguaje fue creado.

En efecto, la misma sintaxis de Python obliga al uso de indentación- es un requisito, no una convención de estilo opcional.  ¿Y porqué importaría algo tan "frivolo" en la programación? En lo particular, en mi experiencia docente en cursos donde se utiliza la programación (he facilitado materias como "simulación y modelos", donde usamos extensivamente el lenguaje R, el cual por cierto no usa indentación), me he dado cuenta que un error de programación muy común para principiantes es el no cerrar una llave/corchete que se ha abierto, o cerrarla en el lugar incorrecto.  En Python, este tipo de error es imposible y este tipo de cosas hace la experiencia de aprender a programar mucho menos frustrante.  Al fin del día, se trata de reducir al máximo las cantidad de horas/programador necesarias para desarrollar un programa, y esto también es una premisa fundamental de Python.

¿Piensan los desarrolladores de Python ceder un poco con la indentación de Python e incorporar el uso de llaves (braces)?  Para responder esta pregunta, abra una consola de Python y teclee lo siguiente:

from __future__ import braces


sábado, 11 de junio de 2011

Listas por comprensión

Yo pensaba que las facilidades para trabajar con vectorización, seleccionando en una sola instrucción todos aquellos elementos de un vector o lista que satisfacen cierto criterio, era algo exclusivo a R que no existía en Python. Como novato de Python, estaba muy equivocado - Python no deja de sorprender ...

La trahison des images (ceci n'est pas une pipe) - 1928/1929
René Magritte

Resulta que eso en Python se llama "listas por comprensión" y la sintaxis de este tipo de expresiones es muy parecida a la notación de conjuntos matemática (definición de conjuntos por comprensión), de modo que es muy intuitivo.  Nos referimos pues a dicha notación identificando algunos de sus elementos:

  • Función de salida: son expresiones que involucran a la variable x y que vienen a ser los elementos que constituyen el nuevo conjunto (o lista) a definir.
  • Variable: es una variable dummy que figura en toda la expresión.
  • Conjunto de entrada: es el conjunto (la lista) a partir de la cual se toman los elementos x.
  • Predicado: es una condición lógica que deben verificar los elementos x para que, aplicándoles f(x), formen parte del nuevo conjunto 8la nueva lista)
En python, la función de salida se representa mediante una expresión, la expresión "x pertenece a A" se indica mediante un constructo Python (for x in A) y el predicado es un constructo Python con if: if p(x), donde p(x) es una expresión que genera un booleano.  Así por ejemplo, si a partir de una con todos los numeros naturales entre 1 y 5, quisieramos generar una nueva lista con cada número de esos elevado al cuadrado, la expresión matemática y la correspondiente expresión en python serían:

A=range(1,6)
[x**2 for x in A]

lo cual arrojaría la siguiente lista en la consola de comandos de Python:

[1, 4, 9, 16, 25]

Nótese que se ha omitido la clausula if (el predicado), aunque esta expresión sería totalmente equivalente a la anterior:

[x**2 for x in A if 1<=x<=5]

Otro ejemplo, supóngase que A es el conjunto de todos los números enteros entre -20 y 20.  ¿Cuales de entre esos números son tales que elevados al cuadrado, están entre 50 y 90?


A=range(-20,21)
[x for x in A if 50<=x**2<=100]

lo cual genera:
 [-8, -9, 8, 9]


Python no es el único lenguaje con listas por comprensión.  Otros lenguajes funcionales como Haskell también incluyen este tipo de constructos.

Gramaticas de sufijo, parte 3: implementación en python

Sin más preámbulos, coloco el código Python que, dado una cadena S que se requiere transformar a T mediante una gramática de sufijo (conjunto de NR reglas), hace una búsqueda exhaustiva de todas las posibles maneras de transformar S a T:


#Definir estructura de datos (clase) para el arbol de busqueda
class Arbol:
    def __init__(self, nodo):
        self.nodo = nodo
        self.subarbol=[]
    def __str__(self):
        return str(self.nodo)
    def agregar_hijo(self,regla,hijo):
        self.subarbol.append(tuple([regla,hijo]))
    def contiene_nodo(self,nodo):
        """contiene el arbol al argumento como nodo?"""
        if self.nodo==nodo:
            return True
        else:
            if self.subarbol!=[]:
                cn=False
                for i in self.subarbol:
                    cn=cn or i[1].contiene_nodo(nodo)
                return cn
            else:
                return False           
    def aplicar_reglas(self,arbol):
        #primero aplica todas las reglas al nodo raiz
        for i in range(0,len(reglas)):
            su0=reglas[i][0]
            su1=reglas[i][1]
            no=self.nodo
            if no.endswith(su0):
                nuevo = no[:no.rfind(su0)]+su1
                if nuevo==T or not(arbol.contiene_nodo(nuevo)):
                    self.agregar_hijo(i,Arbol(nuevo))
        #luego se ramifica recursivamente para cada subarbol
        #(si hay subarboles hijos)
        if self.subarbol!=[]:
            for i in self.subarbol:
                i[1].aplicar_reglas(arbol)
    def recorrer_arbol(self,ruta):
        """recorre todo el arbol retornando las rutas que terminan en T"""
        global rutas
        if self.nodo==T:
            rutas.append(ruta)
        else:
            for i in self.subarbol:
                i[1].recorrer_arbol(ruta+[(i[0],i[1].nodo)])

def imprimir_rutas():
    if rutas==[]:
        print "No es posible transformar",S,"en",T,"mediante esta gramatica."
    else:
        for i in range(0,len(rutas)):
            print "Transformacion",i+1,":"
            print S,"\t-->",
            for j in rutas[i]:
                print j[1],"\tRegla",j[0]+1,": (",
                print reglas[j[0]][0],"-->",reglas[j[0]][1],")"
                if not(j==rutas[i][-1]):
                    print "\t-->",
               
#PROGRAMA PRINCIPAL
#Lee S, T y las reglas del archivo
archivo=open("suffix.in","r")
s=archivo.readline().split(" ")
S=s[0]
T=s[1]
NR=int(s[2])
reglas=[]
for i in range(0,NR):
    s=archivo.readline().strip().split(" ")
    reglas+=[tuple(s)]
archivo.close()
#Genera el arbol de busqueda
ab=Arbol(S)
ab.aplicar_reglas(ab)
#recorre el arbol de busqueda para generar las rutas que
#culminan en T
rutas=[]
ab.recorrer_arbol([])
imprimir_rutas()


Este script requiere de un archivo en donde se coloca la cadena inicial S, la cadena final T y el número de reglas (NR) en la primera línea, separados por espacio.  Las siguientes NR líneas del archivo contienen las reglas gramaticales.  Para probar el script, tome las siguientes líneas y cree con ellas un archivo llamado "suffix.in" en el mismo directorio que "suffix.py" (el script de arriba):

AA CB 6
C B
AB BA
AA CC
CC BB
C A
A B

Con esta data de entrada en el archivo "suffix.in", el programa generaría la siguiente salida:

Transformacion 1 :
AA     --> CC     Regla 3 : ( AA --> CC )
       --> CB     Regla 1 : ( C --> B )
Transformacion 2 :
AA     --> CC     Regla 3 : ( AA --> CC )
       --> CA     Regla 5 : ( C --> A )
       --> CB     Regla 6 : ( A --> B )

miércoles, 8 de junio de 2011

Gramaticas de sufijo, parte 2: la necesidad de arboles n-arios

Una forma de construir un algoritmo para buscar todas las formas posibles para transformar la cadena S a la cadena T sería esta: partiendo de la cadena inicial S verificamos cuales son las reglas aplicables.  Al aplicar dichas reglas a S obtenemos un conjunto de cadenas que "derivan" de ella.  A su vez, verificamos cuales son las reglas aplicables a cada una de las cadenas derivadas, las aplicamos y obtenemos otras cadenas , ...

Este proceso generaría una especie de árbol de búsqueda, donde un nodo representa una cadena, los arcos que salen de ese nodo representan la aplicación de determinada regla gramatical y estos arcos son aferentes a otros nodos, los cuales derivan del nodo padre mediante la aplicación de reglas.  Este árbol no sería del todo diferente al árbol de búsqueda que generan los programas de ajedrez cuando evalúan todas las posibles jugadas y respuestas del oponente en cada turno.  Para el ejemplo del post anterior, el árbol de búsqueda sería como este:
        
              AA
             /     \
            /       \
          AB     CC
           |         |
          BA     BB
           |
          BB

Se necesita etiquetar cada arco del árbol con el número de regla que produjo la transformación.  Observe que si hay muchas reglas, cada nodo podría tener muchos "hijos"- de ahí que necesitamos una estructura de datos parecida a un árbol n-ario, pero con los arcos etiquetados según la regla aplicada en cada transformación.  También hay que observar que en el proceso de verificar cuales reglas aplican a una cadena, pudiésemos obtener una cadena que ya ha sido producida y ya se encuentra en alguna parte del árbol.  Si agregamos nodos duplicados al árbol, el proceso de generar el árbol no terminaría nunca por la circularidad.  Por otro lado, cuando generamos un nodo igual a la cadena T, no se siguen generando más nodos hijos.  De hecho, aquellos nodos iguales a la cadena T son los únicos que pueden figurar repetidamente en el árbol y siempre se van a encontrar al final de alguna rama.  Si ninguna de las ramas del árbol termina en T, sabremos que no ha sido posible transformar S a T mediante la gramática dada.

Observese también que  una cadena (nodo) podría tener más de dos cadenas derivadas de ella, por lo cual no nos serviría un árbol binario como el que se describe en el capitulo 20 de "Pensar como programador".  Entonces, ¿como crear un árbol n-ario (n-ary tree) en python? Una busqueda por google me arrojo una página donde encontré esta definición de clase:


class Arbol:
    def __init__(self, nodo):
        self.nodo = nodo
        self.subarbol=[]
    def __str__(self):
        return str(self.nodo)
    def agregar_hijo(self,hijo):
        self.subarbol.append(hijo)

Para usar esta clase, creamos varias instancias de arboles, digamos a,b y c:
a=Arbol("arbol A")
b=Arbol("arbol B")
c=Arbol("arbol C")


luego, si por ejemplo queremos agregar los arboles b y c como hijos de a, haríamos lo siguiente:

a.agregar_hijo(b)

a.agregar_hijo(c)

Si queremos acceder a los hijos de a, navegariamos por la lista a.subarbol.  A su vez podemos agregarle nietos a a  (hijos de b o c) .  Este fue el punto de partida para desarrollar el programa que resuelve el problema de las gramaticas de sufijo.

continuara...

Gramáticas de sufijo

En la final del Mundial de Programación de la ACM (edición 2009), apareció un encantador problemita sobre gramáticas de sufijo, que es el problema K (suffix grammars) del problemario

Remontándonos a nuestras clases de lenguaje y castellano, un sufijo es una terminación de una palabra y la gramática es el conjunto de reglas que rigen la correctitud de las producción de secuencias de palabras.  En el contexto de la matemática y los lenguajes formales, estos dos conceptos tienen un significado parecido. En el problema en cuestión, una palabra es una secuencia (o cadena) de símbolos, los sufijos son las terminaciones de las palabras y la gramática va a ser el conjunto de reglas que nos van a permitir operar sobre los sufijos de una palabra y sustituirlo por otro sufijo.

Ejemplo:
Supóngase que tenemos una palabra inicial S="AA" que queremos transformar, mediante reglas gramaticales en una palabra T="BB", teniendo para ello las siguientes cuatro reglas:

Regla 1: "A" --> "B"
Regla 2: "AB" --> "BA"
Regla 3: "AA" --> "CC"
Regla 4: "CC" --> "BB"

Una manera de hacerlo sería, partiendo siempre de "AA", utilizar la regla 1 ("A" --> "B") y transformar "AA" en "AB". Luego aplicar la regla 2 para transformar el sufijo "AB" (o sea, toda la palabra) en "BA". Y por último, aplicando la regla 1 nuevamente, "BA" se transforma en "BB".

Otra manera (más directa) sería aplicar las reglas 3 y 4 sucesivamente, lo cual da la transformación en 2 pasos : "AA" --> "CC" --> "BB".

Se quiere desarrollar un programa en python que, dada una cadena inicial S, una cadena final T y un conjunto de reglas (la gramática), encuentre todas las transformaciones de S a T, mediante la aplicación de las reglas gramaticales, por supuesto.

continuará...

martes, 7 de junio de 2011

Sobre el orígen del nombre Python, una descripción de la descripción del titulo de este blog y una digresión sobre las pinturas de Anatoli Fomenko

Se me olvidaba algo importante- ¿porqué este lenguaje se llama Python?  Ciertamente, la pitón ("python" en ingles) es una serpiente muy grande, pero el lenguaje no se llama así en alusión a las culebras, aunque también se hacen muchos juegos de palabras con eso.  Python proviene del nombre de un programa de comedia británico muy popular en la década de los 70 y 80, del cual el creador del lenguaje, Guido Van Rossum es fanático.  Si les gusta el humor negro británico, les recomiendo ampliamente que busquen algunos episodios del "Monty Python's Flying Circus". 


Las gráficas en esta entrada del blog son de un artista ruso, también matemático, llamado Anatoli Fomenko (artista matemático? o matemático artista?)  Sus pinturas son fuertemente inspiradas en la obra de Salvador Dali.  Sus pinturas evocan distopias y el lado oscuro del surrealismo.




Primera entrada -bibliografía básica sobre Python

Existen actualmente dos dialectos de Python: Python 2.x y Python 3.x. Hablar de las diferencias entre uno y otro dialecto ameritarían otra entrada en este blog pero digamos que Python 3.x es la versión que está siendo desarrollada y soportada mientras que Python 2.x ya no verá actualizaciones pero se continúa soportando por razones de compatibilidad hacia atrás. Esto lo aclaro porque si deseas descargar el interprete Python más el entorno de desarrollo para Windows o para Mac desde la página oficial (https://www.python.org/downloads/), debes escoger entre una u otra versión. Si utilizas Linux, ya tienes Python 2 instalada y si deseas trabajar con Python 3, debes descargar la respectiva paquetería. Seguidamente, les presento alguna bibliografía para iniciarse en el estudio de Python con mis respectivos comentarios.
  • Introducción a la programación con Python - Este es un texto utilizado en los cursos de programación de la Universitat Jaume I. Arranca desde cero - ¿que es una computadora? ¿qué es un algoritmo? ¿qué es un programa?, y abarca las estructuras de datos y las estructuras de control fundamentales. No aborda tópicos de programación orientada a objetos ni otros temas más avanzados, pero es muy bueno para iniciarse.
  • Doma de Serpientes para Niños - Aprendiendo a programar con Python - Otro texto introductorio muy bueno, orientado a los niños. Aborda algunos temas básicos relativos a la creación de gráficos (gráficos de tortuga y gráficos 2d). La versión del texto en este enlace es para Linux, pero también está disponible una versión para Windows y para Mac. Estos libros utilizan la versión Python 3.
  • Python para todos - Un texto un poquito más avanzado que los dos anteriores.
  • Aprenda a pensar como programador con Python - Este texto es el resultado de la insatisfacción de unos profesores con los libros de programación para cursos introductorios. Ellos proponen que el Python es el lenguaje ideal para los cursos básicos de programación, porque permite ver un “más alto nivel de éxito y un bajo nivel de frustración" que puede “avanzar rápido con mejores resultados”.
A pesar de que Python es un lenguaje particularmente clemente para los principiantes, es un lenguaje de altísimo nivel muy poderoso. Ciertamente, en cuanto a lenguaje de scripting, quizás no pueda competir con Perl en su terreno, pero, a diferencia de Perl, su sintaxis es sumamente legible, lo cual lo hace particularmente bien adaptado a desarrollar versiones beta de aplicaciones complejas en muy corto tiempo. Quizás por esto es que empresas como Google han utilizado Python y buscan siempre programadores expertos en este lenguaje.