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

Cecilia Music Game

La semana pasada se desarrolló PyWeek 7. Este año participé con una entrada solo (se puede participar solo o en equipo) e hice un pequeño juego en las 10 horas que tuve para dedicarle a la competencia: Cecilia Music Game. Un juego en el que escuchamos una canción y luego tenemos que intentar reproducir eligiendo cuerdas de distinto largo (el tema de la competencia era "The length of a piece of string").

Download: cecilia_music_game-1.0

Video: http://www.youtube.com/watch?v=Z5P0GB00RpY (gracias, Ema)


Decoradores en Python (I) - Introducción

Este artículo es el primero de un plan de 3 artículos. Empezamos con una introducción a los decoradores en Python.

Funciones

Cómo todo en Python, las funciones son objetos. La forma más común de crear un objeto de tipo <function> es mediante el keyword def:

def saludo():

    print "Hola"

Al realizar esta definición, el cuerpo de la función es compilado pero no ejecutado y el objeto de tipo <function> es asociado al nombre 'saludo'. Mediante este nombre podemos referirnos al objeto:

>>> saludo

<function saludo at 0xb7d82fb4>

y utilizando la notación de paréntesis podemos llamar a (ejecutar) la función.

>>> saludo()

Hola

Una función puede tener parámetros (los parámetros son nombres a los que podemos referirnos en el cuerpo de la función):

def saludo2(nombre):

    print "Hola %s" % nombre
def saludo3(nombre, apellido):

    print "Hola %s %s" % (nombre, apellido)

Cuando llamamos a la función con argumentos (los argumentos son valores que en principio se asocian uno a uno a los parámetros de la función):

>>> saludo2("Ceci")

Hola Ceci
>>> saludo3("Ceci", "Pucci")

Hola Ceci Pucci

Los últimos n parámetros pueden tener valores por defecto, entonces estas definiciones y sus consiguientes ejecuciones son válidas:

def saludo4(nombre, apellido="Conti"):

    print "Hola %s %s" % (nombre, apellido)
>>> saludo4("Juanjo")

Hola Juanjo Conti
>>> saludo4("Juanjo", "Garau")

Hola Juanjo Garau
def saludo5(nombre="Juanjo", apellido="Conti"):

    print "Hola %s %s" % (nombre, apellido)
>>> saludo5()

Hola Juanjo Conti
>>> saludo5("Mary")

Hola Mary Conti

Los últimos n argumentos pueden ser argumentos nombrados, es decir utilizando el nombre de los parámetros con los que el argumento se debe asociar. En las siguientes ejecuciones se pueden ver ejemplos de esto:

def saludo6(tratamiento, nombre, apellido):

    print "Hola %s %s %s" % (tratamiento, nombre, apellido)
>>> saludo6("Sr.", apellido="Conti", nombre="Juanjo")

Hola Sr. Juanjo Conti
>>> saludo6("Sr.", "Juanjo", apellido="Conti")

Hola Sr. Juanjo Conti

Los parámetros de una función pueden terminar con <nombre> (una tupla con los últimos argumentos posicionales) y/o *<nombre> (un diccionario con los últimos argumentos nombrados).

def saludo7(tratamiento, *args):

    print "Hola %s %s" % (tratamiento, " ".join(args))
>>> saludo7("Sr.", "Juanjo", "Conti")

Hola Sr. Juanjo Conti
>>> saludo7("Sr.", "Juanjo", "Conti", "Garau")

Hola Sr. Juanjo Conti Garau

Notemos que esta forma de definir una función es bastante útil cuando no sabemos el número de argumentos que se recibirán.

La siguiente es la forma más genérica de definir una función:

def saludo8(*args, **kwargs):

    pass

Decoradores

Un decorador es una función 'd' que recibe como argumento otra función 'a' y retorna una nueva función 'b'. La nueva función 'b' es la función 'a' decorada con 'd'.

Supongamos que queremos avisarle a un sistema de seguridad cada vez que se ejecutan las funciones abrir_puerta y cerrar_puerta. Para hacer una simplificación, el aviso simplemente será imprimir por un mensaje en la pantalla. Podemos escribir el siguiente 'decorador':

def avisar(f):

    def inner(*args, **kwargs):

        f(*args, **kwargs)

        print "Se ha ejecutado %s" % f.__name__

    return inner

Las siguientes son las funciones a decorar:

def abrir_puerta():

    print "Abrir puerta"



def cerrar_puerta():

    print "Cerrar puerta"
>>> abrir_puerta()

Abrir puerta

>>> cerrar_puerta()

Cerrar puerta

Y ahora solo nos limitamos a seguir la definción que di al principio de un decorador:

abrir_puerta = avisar(abrir_puerta)

cerrar_puerta = avisar(cerrar_puerta)

Listo!, ambas funciones han sido decoradas:

>>> abrir_puerta()

Abrir puerta

Se ha ejecutado abrir_puerta

>>> cerrar_puerta()

Cerrar puerta

Se ha ejecutado cerrar_puerta

Azúca sintáctica

En Python 2.3, la anterior era la forma de decorar una función. A partir de Python 2.4 se a añadido azúcar sintáctica al lenguaje que nos permite hacer lo mismo de esta forma:

@avisar

def abrir_puerta():

    print "Abrir puerta"



@avisar

def cerrar_puerta():

    print "Cerrar puerta"

Esta es una forma mucho más visual de hacerlo.

Encadenando decoradores

La decoración de funciones puede encadenarse. Para ejemplificarlo vamos a suponer ahora que solo usuarios autenticados en el sistema pueden ejecutar las funciones abrir_puerta y cerrar puerta.

Nuevamente hacemos una simplificación. Existe la variable AUTHENTICATED que indica el estado del usuario actual. Si el usuario no está autenticado y se intente ejecutar alguna de las funciones, una excepción es lanzada.

def autenticado(f):

    def inner(*args, **kwargs):

        if AUTHENTICATED:

            f(*args, **kwargs)

       else:

           raise Exception

    return inner

Luego, la definición de abrir_puerta y cerrar_puerta debería ser:

@autenticado

@avisar

def abrir_puerta():

    print "Abrir puerta"



@autenticado

@avisar

def cerrar_puerta():

    print "Cerrar puerta"

Con AUTHENTICATED = True:

>>> cerrar_puerta()

Cerrar puerta

Se ha ejecutado cerrar_puerta

Pero si AUTHENTICATED = False:

>>> cerrar_puerta()

Traceback (most recent call last):

File "<stdin> ", line 1, in <module>

File "<stdin> ", line 6, in inner

Exception

update: 2° entrega.


Serpientes y rubíes

Ja! Levanto el guante del desafío que plantea Gastón en su blog:

juanjo@albus:~$ python

Python 2.5.1 (r251:54863, Mar 7 2008, 03:41:45)

[GCC 4.1.2 (Ubuntu 4.1.2-0ubuntu4)] on linux2

Type "help", "copyright", "credits" or "license" for more information.

1. Dado un array con nombres de persona eliminar los nombre que comienzan con “Pe”:

>>> nombres = ['Pablo', 'Raul', 'Pedro', 'Pepe', 'Ariel', 'TerePe']

>>> [n for n in nombres if not n.startswith("Pe")]

['Pablo', 'Raul', 'Ariel', 'TerePe']

  1. Verificar si el mismo array contiene el nombre “Raul”:

>>> "Raul" in nombres

True

  1. Generar un string con todos los nombres unidos por “-”:

>>> "-".join(nombres)

'Pablo-Raul-Pedro-Pepe-Ariel-TerePe'

  1. Generar un segundo array con los nombres todos en minúsculas ordenado alfabéticamente:

>> sorted([n.lower() for n in nombres])

['ariel', 'pablo', 'pedro', 'pepe', 'raul', 'terepe']

  1. Desordenar el array:

>>> from random import shuffle

>>> shuffle(nombres)

>>> nombres

['Pepe', 'Pedro', 'Ariel', 'Raul', 'Pablo', 'TerePe']

  1. Averiguar si la lista siguiente tiene números pares:

>>> bool([n for n in numeros if n % 2 == 0])

True

  1. Averiguar si toda la lista son números pares:

>>> len(numeros) == len([n for n in numeros if n % 2 == 0])

False

  1. Obtener el producto de una lista de números:

>>> f = lambda x,y: x*y

>>> reduce(f, numeros)

120

  1. Obtener el factorial de 9999:

>>> reduce(f, xrange(1, 10000))

# la respuesta tiene 35656 caracteres.

  1. Averiguar si dos arrays son iguales:

>>> [1,2,3,4] == [1,2,3,4]

True

¿Conclusiones? Creo que las listas por comprensión de la serpiente le gana a los .metodos del rubí. Pero en 6 y 7 perdemos feo :-/ ¿Algún pythonista que reescriba esos ejercicios?


Qué aprendí este PyWeek

Acaba de terminar la sexta edición de PyWeek.

Algo que me gusta mucho de esta competencia es que además de ser votado por tus pares, podés recibir comentarios de ellos. De los 48 comentarios que nos dejaron (el juego más votado tuvo 49 votos) resumo:

    <li><strong>Hard to play:</strong> casi todos se quejaban o al menos comentaban que les resultó difícil de jugar el juego, al menos al principio. Nos recomendaban que el primer nivel nivel sea más fácil, esto ayuda al jugador a no frustrarse.</li>
    
    <li><strong>Volver a empezar:</strong> muchos también se quejaron de que una vez que perdías tenías que volver a empezar. Otro punto para evitar la frustración de nuestros queridos jugadores.</li>
    
    <li><strong>Consistencia:</strong> si todo el juego es basado en mouse, el menú también podría haberlo sido :)</li>
    
    <li><strong>Otras críticas menores:</strong> pocos niveles, más robots.</li>
    
    <li><strong>Excelente artwork:</strong> tanto los gráficos como la música fueron muy aclamados. Gracias César, David y Pablo por hacernos ver como estrellas de rock ;-)</li>
    

    Espero que en la próxima tengamos estas cosas presentas para nuestro plan de acción :D ...y que tengamos plan de acción ;-)

    PS: en otro orden de noticias, salimos 6tos.


    Una experiencia en PythonBugDay

    Hoy se llevó a cabo el octavo Python Bug Day. Sabía que era en estos días pero no lo tenía presente ni se me había ocurrido participar. Al mediodía Facundo Batista me lo recuerda por chat y me pregunto... ¿Por qué no? Puedo probar unas horas, ver que pasa, nunca compilé Python tal vez tenga que hacerlo para probar una solución.

    Me dieron una lista de bugs fáciles. Y elejí este: 1779. El bug en particular podría haber sido resuelto en pocos minutos por un desaarrollador de Python. Es más, el mismo había sido reportado por el creador del lenguaje :D Pero como bien dice en uno de los comentarios, es un buen bug para el Python Bug Day. A un experto, resolverlo no le habría costado nada, pero tampoco le habría aportado nada. A un novato como a mi me sirvió para conocer algo del nucleo del lenguaje y el proceso que hay que seguir para resolver un bug:

      <li>Bajar la última versión del código fuente</li>
      
      <li>Compilar</li>
      
      <li>Correr los tests para ver que todo ande bien</li>
      
      <li> Encontrar el bug y arreglarlo</li>
      
      <li>Correr los tests nuevamente</li>
      
      <li>Arreglar los tests que fallan</li>
      
      <li>Agregar tests que prueben el arreglo</li>
      
      <li>Correr los tests nuevamente</li>
      
      <li>Armar un parche</li>
      
      <li>Envair el parche</li>
      

    El bug, que continuará en las versiones previas de Python pero ya está corregido en Python 3000, es el siguiente:

    >>> int("- 1")

    -1

    >>> float("- 1")

    Traceback (most recent call last):

    File "<stdin>", line 1, in <module>

    ValueError: invalid literal for float(): - 1

    Cuando se quiere crear un número a partir de un string, no pueden haber espacios en blanco entre el signo (+ o -) y el número. El comportamiento correcto es el de la función float. El de int es errorneo. Necesita ser arreglado.

    Obtener el código fuente

    Bajé del servidor svn la última versión de Python 3000, la nueva versión del lenguaje, para el cual el bug estaba abierto:

    svn co http://svn.python.org/projects/python/branches/py3k/

    (Esto llevó bastante tiempo. Mientras tanto cociné y almorcé.)

    Compilar

    cd py3k/

    ./configure && make

    Correr el intérprete compilado

    ./python

    Correr los tests

    make test

    o

    ./python Lib/test/regrtest.py

    Arreglar el bug

    En los comentarios sel bug se decía que era simple de resolver y que no consistía más que en comentar 2 líneas en un archivo en C.

    Revisé el archivo Objects/longobject.c y luego de entender como funcionaba la función PyLong_FromString(char str, char *pend, int base), comenté las líneas 1688 y 1689. Problema resuelto!

    Compilé para que mis cambios se vean reflejados.

    make

    Y probé en el intérprete.

    >>> int("- 1")

    Traceback (most recent call last):

    File "<stdin>", line 1, in <module>

    ValueError: invalid literal for int() with base 10: '- 1'

    Perfecto!

    Luego corrí las pruebas afectadas por mi cambio y fallaron.

    ./python Lib/test/test_builtin.py

    La función en particular era test_int(self). Luego de una mirada rápida, comenté los valores de una lista que hacían fallar la prueba y pensé que con eso era suficiente. Consulté y me dijeron que también agregue mi cambio en Misc/NEWS. Lo hice. Lo siguiente era generar el parche.

    svn diff | tee bug_fix_for_1779.diff

    El mismo está en http://bugs.python.org/file9220.

    Luego lo vio Facundo y me dijo que tenía que tenía que agregar pruebas para mi parche. Me llevó bastante tiempo entender como funcionaba la prueba. Finalmente, luego de preguntar bastante conseguí el resultado deseado. Volví a generar un parche y a subirlo.

    svn diff | tee bug_fix_for_1779-plustests.diff

    http://bugs.python.org/file9228

    La experiencia

    La verdad es que la experiencia fue muy buena. En unas 5 horas aprendí mucho sobre Python en si y sobre su ciclo de desarrollo, interactué con una comunidad despuesta a darte una mano cuando la necesitás y arreglé un error en el lenguaje que más uso. Voilá! Muchas gracias Comunidad de Python por organizar este Python Bug Day.


    Manipulación de pixels con Python-Fu

    Usando GIMP y el complemento que permite crear plug-ins en Python podemos hacer manipulación a bajo nivel de los pixels de una imagen.

    PixelRegion

    PixelRegion permite realizar una abstracción sobre una imagen de forma tal de verla como una región de pixeles. Cada posición x,y de la región corresponde a un pixel de la imagen. En el caso de las imágenes RGB, cada pixel tiene 3 valores. En Python-Fu estos valores son representados con un string de 3 caracteres en el que cada caracter es uno de aquellos que tiene su número de orden (función ord) entre y 255.

    Los siguientes ejemplos son una demostración sencilla de como trabajar con PixelPerfect para manipular imágenes. En el primer ejemplo se leen los valores de algunos de los pixeles de la imagen y en el segundo se escriben otros.

    Ejemplo 1 (lectura)

    La siguiente imagen de 5x5 pixeles (ampliada) será usada para este ejemplo:

    5×5 pixel image

    Imagen original

    La función pixel es la que se ejecutará cuando ejecutemos el plug-in creado para este ejemplo. No se ingresan parámetros desde la interfaz gráfica del mismo, pero la función recibe 2 argumentos. Objetos que representan la imagen y la capa (layer) a procesar.

    def pixels(img, layer):
    
    
    
        w = layer.width
    
        h = layer.height
    
    
    
        pr = layer.get_pixel_rgn(0, 0, w, h)
    
    
    
        print "Valores de los pixeles de las esquinas de la imagen"
    
    
    
        r,g,b = pr[0,0]
    
        print [ord(x) for x in r,g,b]
    
    
    
        r,g,b = pr[0,4]
    
        print [ord(x) for x in r,g,b]
    
    
    
        r,g,b = pr[4,0]
    
        print [ord(x) for x in r,g,b]
    
    
    
        r,g,b = pr[4,4]
    
        print [ord(x) for x in r,g,b]
    
    
    
        layer.update(0, 0, w, h)
    
        layer.flush()
    
        gimp.displays_flush()

    El ejemplo es sencillo y lo que hace es instanciar una PixelRegion tan grande como la capa (layer). Luego los diferentes pixeles de la misma pueden accederse mediante sus coordenadas: pr[x,y].

    Se muestran los valores RGB de cada uno de los puntos del ejemplo.

    Código fuente: pixels-read.py

    Ejemplo 2 (escritura)

    En una porción de código que empieza de forma similar a la del ejemplo anterior podemos ver que una vez que hemos obtenido la PixelRegion que nos interesa, podemos escribir pixeles en ella.

    El siguiente ejemplo aplicado sobre la imagen del ejemplo anterior dibuja una equis amarilla:

    def pixels_write(img, layer):
    
    
    
        w = layer.width
    
        h = layer.height
    
        pr = layer.get_pixel_rgn(0, 0, w, h)
    
    
    
        yellow_rgb = (255,255,0)
    
        yellow_str = chr(yellow_rgb[0]) + chr(yellow_rgb[1]) + chr(yellow_rgb[2])
    
    
    
        for i in xrange(5):
    
            pr[i,i] = yellow_str
    
            pr[4-i,i] = yellow_str
    
    
    
        layer.update(0, 0, w, h)
    
        layer.flush()
    
        gimp.displays_flush()

    5×5 image with yellow pixels

    Imagen procesada con el código del ejemplo 2

    Código fuente: pixels-write.py

    Links



    Ejemplos de PLY

    Acabo de crear un sitio web con todos los ejemplos de PLY que acompañan su distribución.

    En distribuciones derivadas de Debian los pueden encontrar en:

    /usr/share/doc/python-ply-doc/examples/

    luego de haber instalado python-ply y su documentación:

    apt-get install python-ply python-ply-doc

    PLY Examples

    Los archivos .py de los ejemplos se encuentran en una versión html con su sintaxis coloreada. Esto lo hice con el comando pygmentize. Para poder usarlo, en Debian y similares, debemos instalarlo el paquete python-pygments. Más información.

    En particular utilicé OpenOffice para editar el README que acompaña la distribución de los ejemplos y crear una página html y este oneliner para crear todos los .py.html de una pasada:

    for d in ls -d * | grep -v index.html; do cd $d; for f in ls *.py; do pygmentize -f html -O full -o $f.html $f; done; cd ..; done

    Espero sea útil, cree el sitio a partir de una sugerencia en la lista de correos de PLY.


    MiniLisp (un ejemplo de ply)

    PLY

    The asteroid to kill this dinosaur is still in orbit.

    • Lex Manual Page

    PLY o python-ply (cómo se llama su paquete en Debian) es una implementación de las herramientas lex y yacc para análisis léxico y sintáctico. Está enteramente escrito en Python y su primera versión fue desarrollado por David Beazley en el año 2001 para ser usado en un curso de Introducción a los Compiladores.

    Lex

    Lex es una creador de analizadores léxicos (lexers). La función principal de un lexer es tomar un flujo de caracteres entrada y devolver un flujo de tokens como salida. Ejemplos de tokens en un programa escrito en algún lenguaje de programación podrían ser: un número, un paréntesis, un identificador o una palabra clave. Por ejemplo: 17, ), miVarible, if.

    Para definir los tokens utilizamos expresiones regulares.

    Yacc

    Yacc, Yet Another Compilers Compiler, nos permitirá crear un programa que tome un flujo de tokens como entrada y reconozca a partir de ellos un lenguaje. Notemos por ejemplo que si bien if { 555 ;; for printf i++[] es un flujo de tokens válidos de C, no es una sentencia válida del lenguaje como si lo es for(i=0; i<5; i++){}.

    Para definir la gramática de un lenguaje de programación vamos a usar una notación conocida como BNF (Backus–Naur form).

    MiniLisp

    Intérpretes de lenguajes de programación como Python o PHP son escritos en C por razones de eficiencia. ¿Vamos a usar Python para escribir uno? Bueno.. ¿por qué no? La filosofía de Python consiste en escribir rápido una solución para un problema y poder probarla enseguida. En el tiempo en que implementás en C una idea podes escribir 3 soluciones diferentes en Python, probarlas y elegir con cual continuar. En caso de que en algún momento se detecte que el programa resultante corre lento (más lento de lo necesario), siempre podés:

    • realizar optimizaciones en el código Python
    • reescribir en C alguna parte critica del mismo.

    El intérprete que voy a construir para aprender ply va a tener sabor a Lisp y va a ser muy sencillo. Va a ser un MiniLisp! En particular:

    • Usará notación prefija mediante paréntesis de la forma (fun-name arg [arg]). Esto significa que el intérprete podrá resolver expresiones como (+ 1 1) y responderá 2, (= 1 1) y responderá #t (la forma en que voy a representar el valor de verdad True), pero también (+ 1 2 3 4 5) que da como resultado 15 y (or #t #f #f) que da como resultado #t.
    • Permitirá resolver expresiones anidadas. Esto significa que además de las expresiones anteriores, podrá resolver expresiones como (+ 1 (+ 2 2) (- 5 4) 10) que da como resultado 16, (and (= 1 1) #t) que da como resultado #t y (and (or (= (+ 10 10) 20) #f (= #t #f)) (= 13 (+ 10 1 1 1 1 (- 2 1)))) cuyo resultado se deja a cargo del lector para que vaya entrando en clima :)
    • Contará con funciones de manejo de listas típicas en Lisp como car, cdr y cons:
      • (car '(1 2 3)) obtiene el primer elemento (o cabeza) de la lista '(1 2 3): 1
      • (cdr '(1 2 3)) obtiene la cola de la lista '(1 2 3): (2 3)
      • (cons 0 '(1 2 3)) crea una nueva lista con 0 como cabeza y '(1 2 3) como cola: (0 1 2 3)
      • Notar que (cons '(1 2) '(3 4)) da como resultado una lista cuya cabeza es la lista '(1 2) y su cola '(3 4): ((1 2) 3 4)
      • También se podrá usar (concat '(1 2) '(3 4)) para obtener una lista que sea la concatenación de ambas: (1 2 3 4)
      • y (list 1 2 3 4) para crear una lista de elementos: (1 2 3 4)
    <li>La implementación tiene solo fines didácticos así que solo trabajará con enteros aunque añadir soporte para números reales (floats) debería ser fácil.</li>
    
    <li>No tendrá un manejo de errores muy completo.</li>
    
    <li>Pensé varias formas de implementar la función <code>define</code> pero no conseguí que funcione del todo bien :(, así que quité la funcionalidad.</li>
    
    <li>Otra función común en las implementaciones de Lisp con la que contará MiniLisp es cond, la cual evalúa su primer argumento y si es verdadero retorna el segundo. Esta función puede usarse para implementar construcciones de control de flujo más complejas como if-else, while o for. Eso si en MiniLisp se pudieran definir funciones :(
    
    • (cond (= 1 1) 7) retorna 7
    • pero (cond (= 1 2) 7) no retorna nada.

    Manos a la obra

    PLY consiste en un paquete que contiene los módulos ply y yacc.

    En Debian/Ubuntu podemos instalarlo con el comando (como root o mediante sudo)

    apt-get install python-ply

    lex.py

    Al programa que generará un analizador léxico lo llamé lex.py y consiste en:

    import ply.lex as lex

    Debemos crear una tupla con todos los nombres de los tokens a reconocer (por convención escribimos los nombres en mayúsculas):

    tokens = ('QUOTE', 'SIMB', 'NUM', 'LPAREN', 'RPAREN', 'NIL', 'TRUE', 'FALSE', 'TEXT')
    
    

    Un diccionario en el cual la clave es una palabra reservada y el valor uno de los tokens de la tupla anterior:

    reserved = {
    
    'nil' : 'NIL',
    
    }

    En este ejemplo prácticamente no hay palabras reservadas, otros lenguajes podría haber palabras como if, while, for o return.

    Lo siguiente es definir las expresiones regulares para cada token. Existen dos formas de hacerlo, mediante strings o mediante funciones.

    El primer caso se usa cuando el token no requiere ningún tipo de procesamiento luego de ser econtrado:

    t_LPAREN = r'\('
    
    t_RPAREN = r'\)'
    
    t_QUOTE = r'\''
    
    t_TRUE = r'\#t'
    
    t_FALSE = r'\#f'

    Notar que se usan raw strings de Python para escribir las expresiones regulares que posteriormente serán compiladas y usadas (PLY utiliza el módulo re en su análisis léxico).

    Para los tokens correspondientes a números podemos querer hacer alguna verificación antes de devolverlo, en ese caso la especificación del token puede hacerse mediante una función:

    def t_NUM(t):
    
        r'd+'
    
        try:
    
            t.value = int(t.value)
    
        except ValueError:
    
            print "Line %d: Number %s is too large!" % (t.lineno,t.value)
    
            t.value = 0
    
        return t

    Notar que en el docstring de la función se debe colocar la expresión regular correspondiente al token.

    Otro ejemplo de esto se da para el token SIMBOL, pero con una particularidad. Este token se usa para los nombres de funciones o variables. car, cdr o and son ejemplos de símbolos en MiniLisp.

    def t_SIMB(t):
    
        r'[a-zA-Z_+=*-][a-zA-Z0-9_+*-]*'
    
        t.type = reserved.get(t.value,'SIMB')    # Check for reserved words
    
        return t

    Luego de encontrar una secuencia de caracteres que corresponda con la expresión regular de los símbolos, nos fijamos que no sea una palabra reservada. Si lo es, en t.type se guardará el nombre de token correspondiente, por ejemplo 'NIL', caso contrario 'SIMB'.

    Si hubiesemos especificado t_NIL = r'nil' en lugar de usar el diccionario reserved, cadenas de caracteres como nillave (un símbolo válido) serían interpretadas como NIL seguido del símbolo lave.

    El orden en que estas definiciones son usadas es el siguiente: primero los strings en orden descendiente de la longitud de la expresión regular y luego las funciones en el orden que fueron escritas.

    Archivo completo: lex.py

    yacc.py

    El código en el que se define la gramática del lenguaje lo puse en un archivo llamado yacc.py. El siguiente BNF expresa la gramática de MiniLisp (las palabras en mayúsculas representan símbolos terminales y las palabras en minúsculas símbolos no terminales), a la izquierda de la regla siempre va un único elemento y el símbolo ::= puede leer se como 'es':

    exp ::= atom
    
    exp ::= quoted_list
    
    exp ::= call
    
    quoted_list ::= QUOTE list
    
    list ::= LPAREN items RPAREN
    
    items ::= item items
    
    items ::=
    
    item ::= atom
    
    item ::= list
    
    item ::= quoted_list
    
    item ::= call
    
    call ::= LPAREN SIMB items RPAREN
    
    atom ::= SIMB
    
    atom ::= bool
    
    atom ::= NUM
    
    atom ::= TEXT
    
    atom ::=
    
    bool ::= TRUE
    
    bool ::= FALSE
    
    atom ::= NIL

    y lo siguiente es cómo queda la gramática expresa en código. Se debe definir una función por cada una de las reglas previas (el docstring de la función corresponde a la regla). Cada función recibe como parámetro un objeto iterable (muy parecido a una lista, pero que no se comporta totalmente como tal, así que cuidado con los subíndices negativos) que contiene los valores de cada símbolo de la regla.

    # BNF
    
    
    
    def p_exp_atom(p):
    
        'exp : atom'
    
        p[0] = p[1]
    
    
    
    def p_exp_qlist(p):
    
        'exp : quoted_list'
    
        p[0] = p[1]
    
    
    
    def p_exp_call(p):
    
        'exp : call'
    
        p[0] = p[1]
    
    
    
    def p_quoted_list(p):
    
        'quoted_list : QUOTE list'
    
        p[0] = p[2]
    
    
    
    def p_list(p):
    
        'list : LPAREN items RPAREN'
    
        p[0] = p[2]
    
        f = p[2][0]
    
    
    
    def p_items(p):
    
        'items : item items'
    
        p[0] = [p[1]] + p[2]
    
    
    
    def p_items_empty(p):
    
        'items : empty'
    
        p[0] = []
    
    
    
    def p_empty(p):
    
        'empty :'
    
        pass
    
    
    
    def p_item_atom(p):
    
        'item : atom'
    
        p[0] = p[1]
    
    
    
    def p_item_list(p):
    
        'item : list'
    
        p[0] = p[1]
    
    
    
    def p_item_list(p):
    
        'item : quoted_list'
    
        p[0] = p[1]
    
    
    
    def p_item_call(p):
    
        'item : call'
    
        p[0] = p[1]
    
    
    
    def p_call(p):
    
        'call : LPAREN SIMB items RPAREN'
    
        if DEBUG: print "Calling", p[2], "with", p[3]
    
        p[0] = lisp_eval(p[2], p[3])
    
    
    
    def p_atom_simbol(p):
    
        'atom : SIMB'
    
        p[0] = p[1]
    
    
    
    def p_atom_bool(p):
    
        'atom : bool'
    
        p[0] = p[1]
    
    
    
    def p_atom_num(p):
    
        'atom : NUM'
    
        p[0] = p[1]
    
    
    
    def p_atom_word(p):
    
        'atom : TEXT'
    
        p[0] = p[1]
    
    
    
    def p_atom_empty(p):
    
        'atom :'
    
        pass
    
    
    
    def p_true(p):
    
        'bool : TRUE'
    
        p[0] = True
    
    
    
    def p_false(p):
    
        'bool : FALSE'
    
        p[0] = False
    
    
    
    def p_nil(p):
    
        'atom : NIL'
    
        p[0] = None

    En yacc.py pueden verse todos los detalles de implementación que logran hacer que esta estructura arbórea que podemos armar con yacc pueda manipularse para trabajar como un intérprete de Lisp. La implementación concreta fue hecha mediante funciones sencillas escritas en Python. Traté de mantener el engine del lenguaje lo más simple posible ya que el objetivo principal fue trabajar en el analizador léxico/sintáctico del mismo.

    En particular destaco el uso de las listas de Python (una estructura de datos muy poderosa) como componente fundamental de este pequeño Lisp.

    mini-lisp.py

    Finalmente utilicé el módulo cmd (parte de la librería estándar de Python) para crear la interfaz de línea de comandos del intérprete:

    # -*- coding: utf-8 -*-
    
    from yacc import yacc, lisp_str
    
    import cmd
    
    
    
    class MiniLisp(cmd.Cmd):
    
        """
    
        MiniLisp evalúa expresiones sencillas con sabor a lisp,
    
        más información en https://viejoblog.juanjoconti.com.ar
    
        """
    
    
    
        def __init__(self):
    
            cmd.Cmd.__init__(self)
    
            self.prompt = "ml> "
    
            self.intro  = "Bienvenido a MiniLisp"
    
    
    
        def do_exit(self, args):
    
            """Exits from the console"""
    
            return -1
    
    
    
        def do_EOF(self, args):
    
            """Exit on system end of file character"""
    
            print "Good bye!"
    
            return self.do_exit(args)
    
    
    
        def do_help(self, args):
    
            print self.__doc__
    
    
    
        def emptyline(self):
    
            """Do nothing on empty input line"""
    
            pass
    
    
    
        def default(self, line):
    
            """Called on an input line when the command prefix
    
               is not recognized.
    
               In that case we execute the line as Python code.
    
            """
    
            result = yacc.parse(line)
    
            s = lisp_str(result)
    
            if s != 'nil':
    
                print s
    
    
    
    if __name__ == '__main__':
    
            ml = MiniLisp()
    
            ml.cmdloop()

    Con esto se logra tener algunas funcionalidades útiles como poder usar las flechas izquierda y derecha del teclado para movernos por la línea que estamos escribiendo y las flechas arriba y abajo para movernos por la historia de las expresiones que fuimos introduciendo.

    Resultado

    En las siguientes capturas de pantalla se ven ejemplos de MiniLisp en acción:

    Operaciones básicas

    Minilisp 1

    Manejo de listas

    Minilisp 2

    Ejercicio planteado al lector

    Minilisp 3

    El código fuente completo de MiniLisp está empaquetado en: mini-lisp-0.1.tgz

    También puede navegarse en: https://viejoblog.juanjoconti.com.ar/files/python/mini-lisp/

    Conclusión

    Si bien la implementación lograda dista mucho (en su funcionalidad) de una implementación real de Lisp, su sintaxis (aunque simple como la del propio Lisp) es lo que quería lograr y me sirvió para hacer muchas pruebas (algunos de sus resultados son reflejados en este artículo).

    El objetivo de este desarrollo fue aprender PLY para poder utilizarlo en un proyecto que si bien no es un lenguaje de programación, se le parece en la necesidad de realizar un análisis léxico/sintáctico: un probador de teoremas (ATP).

    Referencias

    Desde el sitio web de ply se puede acceder a su documentación.

    Sobre compiladores: Compiladores: técnicas, principios y herramientas.

    Update: unas palabras sobre reducción

    Supongamos que la expresión que se analizará sintácticamente (ya pasó por el analizador léxico, es decir que consiste de tokens válidos) es: (= (+ 1 1) 2).

    En base a las reglas BNF definidas, esta expresión puede verse como el siguiente árbol:

    La expresión se va resolviendo de abajo hacia arriba en el árbol (o lo que es lo mismo de adentro hacia afuera en la expresión) mediante la aplicación de las funciones definidas.


    irc2html.py

    Ayer colgué del blog el log de un tutorial dictado a través del IRC. Cuando busqué en Google un formateador de logs de irc no encontré lo que buscaba. El principal feature que me interesaba era que se distinga a los diferentes usuarios con diferentes colores. Probablemente podría haber buscado con un poco más de empeño y habría encontrado lo que buscada, al fin de cuentas ya había usado un programa así hace unos años, pero me hubiera perdido de una hora entretenida programando y aprendiendo Python.

    Script irc2html.py

    Primero lo primero. Tal vez llegaste a este blog buscando lo mismo que yo pero más adelante en el tiempo. Good news: irc2html.py. UPDATE: irc2html-f.py es una versión mejorada tras recibir un comentario en este post.

    Uso:

    ./irc2html.py charla.log

    escribe en la salida estándar código html listo para embeber. También podés hacer:

    ./irc2html.py charla.log > charla.html

    (prometo que tu browser no se va a quejar)

    Un color para cada uno

    Como decía más arriba, lo que más me interesaba era que se distinga, mediante colores, lo que los distintos usuarios dicen. Esto se hace más difícil a medida que aumenta el número de usuarios. La dificultad radica en encontrar colores lo suficientemente diferentes como para distinguir a los usuarios. Lo primero en lo que pensé fue en hacer una función que reciba un color inicial y que luego vaya saltando por la paleta de colores a medida que iba necesitando más colores.

    Paleta de Colores

    Hice algunas pruebas pero no prosperaron. Por suerte esto me sirvió como escusa para practicar yield y generadores y Python. Terminé con una implementación mucho menos ambiciosa que la original:

    
    # Generador de colores:
    
    #
    
    # Para ayudar a distingir lo que dice un usuario de lo que dice otro,
    
    # se utilizan distintos colores para cada uno.
    
    #
    
    # La idea original del generador de colores era que vaya saltando por
    
    # la paleta de colores según un patrón matemático.
    
    # La actual implementación es más simple y etática.
    
    
    
    color_list = ['ff0000', 'fff200', '00ff00', '00fff2', '0000ff', 'aa18ff',
    
                  'ff00fb', 'fbb636', 'b1466b', '3d3166',  'bfbf2e', '377972']
    
    
    
    # Lighter colors:
    
    #color_list = ['fff1df', 'feffdf', 'e3ffce', 'ffe4df', 'fcdfff', 'e7dfff',
    
    #              'e7f1ff', 'e7fff9', 'f3ffe7', 'e9e9e9', 'ffedb9', 'f0e9d5']
    
    
    
    def color_gen(l):
    
        colors = l[:]
    
        colors.reverse()
    
        while True:
    
            if colors:
    
                yield colors.pop()
    
            else:
    
                colors = l[:]

    Expresiones regulares

    En el script hago uso de expresiones regulares para parsear las líneas del archivo de log. El archivo sobre el que trabajé tenía dos tipos de líneas. Con mensajes de usuarios, como:

    '<pablo_!~pablo@r190-64-130-143.dialup.adsl.anteldata.net.uy> [17:00] comprendido marga :)\n'

    o de información como:

    '-charm.oftc.net- [17:00] garaguas (~garaguas@host193.200-82-125.telecom.net.ar) joined the channel\n'

    Me interesan la primeras, a las segundas las puedo ignorar. La expresión regular que usé fue:

    '\<(?P<nombre>.+)!.*\>(?P<dicho>.+)$'

    y se lee:

    1) el carácter '<'.

    2) de 1 a n caracteres de cualquier tipo (excepto '\n') y a las cadenas que matchen en esta parte de la expresión voy a referenciarlas luego como 'nombre'.

    3) el carácter '!'.

    4) de 0 a n caracteres de cualquier tipo (excepto '\n').

    5) el carácter '>'

    6) de 1 a n caracteres de cualquier tipo (excepto '\n') y a las cadenas que matchen en esta parte de la expresión voy a referenciarlas luego como 'dicho'.

    7) final de línea ($).

    Un ejemplo en el REPL de Python:

    >>> import re
    
    >>> a = '<pablo_!~pablo@r190-64-130-143....> [17:00] comprendido marga :)n'
    
    >>> b = '-charm.oftc.net- [17:00] garaguas (~garaguas@host...) joined the channeln'
    
    >>> rex = <(?P<nombre>.+)!.*>(?P<dicho>.+)$'
    
    >>> pat = re.compile(rex)
    
    >>> m = pat.match(b)
    
    >>> m
    
    >>> m == None
    
    True
    
    >>> m = pat.match(a)
    
    >>> m
    
    <_sre.SRE_Match object at 0x8707698>
    
    >>> m.group('nombre')
    
    'pablo_'
    
    >>> m.group('dicho')
    
    ' [17:00] comprendido marga :)'

    Resultado final

    Comentando y descomentando las líneas:

    #line_format = "<span style='color:#%s'>%s:</span>%s<br/>"
    
    line_format = "<div style='background-color:#%s'><b>%s:</b> %s</div>"

    se pueden obtener dos resultados distintos:

    Formato 1:

    Chat1

    Formato 2:

    Chat2

    Notas finales

    • Espero este script les haya sido útil, ya sea para formatear logs de irc como para seguir aprendiendo Python (yo en particular casi no había usado yield y con re solo había hecho pruebas).
    • La primer imagen del post es una composición de una captura de pantalla y esta linda imagen lgpl de un gotero.