1. En mi afán de animar un poco el blog, voy a empezar una sección (etiqueta #Monotremas) con las utilidades, aplicaciones, páginas o servicios que más me han llamado la atención en Internet. No descarto poner algún día una chorrada, si me parece graciosa, pero seran raras excepciones. vamos con Los monotremas de la semana:

    HabbitRPG

    HabbitRPG te permite convertir tus metas y tus objetivos en un Juego de Rol. Si lo haces bien, ganas puntos de experiencia y monedas de plata y oro con las que puedes conseguir nuevas armas, escudos, armaduras y demás parafernalia. Si fallas, pierdes puntos de vida; si fallas miserablemente, pierdes todo lo ganado y tienes que empezar de nuevo. Quien sabe, quiza sea el extra de motivación que algunos necesitamos. Tiene una Campaña en Kickstarter para mejorar el producto.

    MyScriptFont

    MyScriptFont es una aplicación web que te permite obtener una fuente o tipografia a partir de tu propia letra manuscrita. Es muy sencillo de usar; te descargas desde la web una plantilla,la rellenas con tu letra, la escaneas y la subes. Tras unos pocos segundos, obtienes un fichero con la tipografía en formato TrueType. Lo que ya no resulta tan sencillo es que tengas una letra bonita que merezca el esfuerzo. Ojo con la orientación, tamaño y posición relativa de los caracteres. Mi primera versión ha sido un truño, si consigo algo mas presentable la publicaré aquí.

    Lino

    Lino es un servicio de publicación y compartición de notas, fotos, vídeos, enlaces, y lo que se ocurra, sobre una pizarra virtual. Es como el tablero de corcho de toda la vida, pero en la nube. Es sencillo de usar y puede ser una gran herramienta de trabajo colaborativo. Los tableros pueden ser privados, públicos pero sólo para lectura (cualquiera puede ver el tablero, pero no modificarlo), o totalmente públicos. El registro es gratuito, y solo necesitas un navegador moderno y/o un smartphone para usarlo. Puedes acceder a una demostración de lino -sin necesida de registrarte- desde su página web.

    0

    Añadir un comentario

  2. ¿Te sientes dolido, casi traicionado por la anunciada muerte de Google Reader? Una rápida búsqueda en Google y parece que su próximo sucesor apunta maneras: Feedly. Como no hay mejor suerte que estar en el sitio preciso en el momento preciso (¿Verdad, Eddard Stark?), los de feedly parece que se venían oliendo que esto iba a pasar, y llevan trabajando en un reemplazo, el proyecto Normandía, una reimplementación compatible a nivel de API de los servidores de GoogleReader, y ejecutandose sobre Google App Engine, por añadir un poco de sal a la herida.

    Feedly tienen aplicaciones nativas para teléfonos iOS, Android y Kindle, y plugins para Chrome y Firefox. Puedes usarlo simplemente registrándote con tu cuenta de Google, y el día fatídico en que se cierre el servicio de Google las aplicaciones empezarán a usar Normandy, con lo cual el usuario no debería notar ningún problema. Parece que han añadido más servidores para evitar el colapso ante la avalancha de usuarios que, presumiblemente, les caerá encima.

    Por lo que he podido ver usa más o menos los mismos atajos de teclado (J/K para avanzar/retroceder, ? para la ayuda), y tiene diferentes formas de visualización, por defecto nos muestra una llamada magazine, bastante atractiva, pero tiene la posiblidad de cambiar a una vista más resumida, title, solo con los títulos de las entradas; esto es imprescindible para poder filtrar rápido cuando tienes, como yo, poco tiempo y muchos feeds. En esta página hay más consejos para sentirte cómodo en feedly si vienes de Google Reader (en Inglés).

    0

    Añadir un comentario

  3. Pygments es una librería, plugin y utilidad, todo en uno, que nos permite embellecer nuestro código, añadiendo resaltado de palabras claves y colores que facilitan la lectura del mismo. Es especialmente útil para foros, wikis, blogs o cualquier sistema de enseñanza on-line que necesiten mostrar código fuente. por ejemplo, lo usa mi repositorio de código favorito, bitbucket.

    Pygments está escrito en Python, pero entiende muchos tipos de lenguaje, incluyendo los lenguajes de programación más populares como Python, Ruby, JavaScript, C, C++, C#, Java, Lua, Perl, lenguajes clásicos como Pascal, Common Lisp o Fortran, lenguajes nuevos como CoffeeScript o Clojure, lenguajes de plantillas como los de Django, Jinja, ERB (Ruby templating), Mako o Myghty y mil cosas más: ficheros de configuración de Apache, hojas de estilo CSS, SQL, etc... Esta variedad viene dada por lo sencillo que resulta escribir nuevos analizadores léxicos; la mayoría de los lenguajes solo necesitan definir algunas expresiones regulares.

    Puede usarse como herramienta de línea de comandos: pygmentize, como librería para uso interno o como plugin o añadido para sistemas ya existentes: Wordpress, MoinMoin y REsT entre otros. Es muy fácil tanto de utilizar como de extender; puedes personalizar la hoja de estilos para reflejar tus gustos estéticos (Pero recuerda, marrón y azul rara vez combinan). La salida más común es Html, pero también podemos obtener la salida en RTF, LaTex o códigos ANSI para la consola.

    Para integrarlo con Blogger usé la línea de comandos para obtener los estilos CSS correspondientes (pygmentize -f html -S colorful) y los incluí en mi plantilla.

    Como creo fervientemente en las virtudes didácticas de los ejemplos, he aquí un cacho de código Python (¡Mi primer código publicado en Python 3!) antes de Pygments:

    granos = 1
    for escaque in range(1, 65):
        print ("En el escaque #{0} hay #{1} granos".format(escaque, granos))
        granos *= 2
    

    Y después:

    granos = 1
    for escaque in range(1, 65):
        print ("En el escaque #{0} hay #{1} granos".format(escaque, granos))
        granos *= 2
    

    Veamos otro ejemplo, esta vez con Ruby. Antes:

    granos = 1
    64.times do |escaque|
        puts "En el escaque #{escaque+1} hay #{granos} granos"
        granos *= 2
        end
    

    Y después:

    granos = 1
    64.times do |escaque|
        puts "En el escaque #{escaque+1} hay #{granos} granos"
        granos *= 2
        end
    

     

    0

    Añadir un comentario

  4. Este post es una traducción del artículo original de Erez, publicado en su blog Stories For Sad Robots. El original puede consultarse aquí:

    http://blog.erezsh.com/how-to-write-a-calculator-in-70-python-lines-by-writing-a-recursive-descent-parser/

    Hay referencias a un artículo anterior, si alguien está interesado un que lo traduzca también que me deje un comentario y se hará lo que se pueda.

    Empieza la traducción

    Hace tres meses, escribí un post detallando el proceso de implementar una calculadora usando una librería de parsing. La respuesta popular, sin embargo, mostró que la mayoría de los lectores estaban más interesados en ver una calculadora implementada desde cero, usando solo las "pilas incluidas". Y me dije ¿Por qué no?

    Implementar una calculadora es fácil, si usas algunos trucos específicos de las expresiones aritméticas, pero usar estos trucos siempre produce lo mismo: La solución pierde elegancia, no se puede extender y se hace difícil de comprender intuitivamente. Como parecía un desafío interesante, y puede conducir a un post útil, he decidido escribirlo usando un analizador descendente recursivo (recursive descent parser en wikipedia) genérico. Con la mismo filosofía de la vez anterior, quiero mantener el menor número de líneas que sea razonable, así que el código estará lleno de trucos y hacks, pero estarán a un nivel superficial y no serán específicos de la tarea que tenemos entre manos.

    Este artículo es una explicación detallada, paso a paso, de la implementación. Si quieres pasar directamente al código e intentar entenderlo por tu cuenta, solo tienes que ir a la última parte. Con suerte una vez leído y entendido el artículo tendrás una mejor comprensión del funcionamiento interno de un analizador sintáctico. Luego puedes incluso utilizar una librería de análisis sintáctico de verdad y ahorrarte todos estos enojosos detalles.

    Para entender este artículo, hace falta tener un conocimiento más o menos completo de Python, y es recomendable tener al menos una idea de lo que son los analizadores sintácticos o parsers y para que sirven. Si no estás seguro, te recomiendo que leas el artículo anterior How To Write A Calculator in 50 Python Lines (Without Eval), en el que se explica la gramática que usaremos aquí.

    Paso 1: Tokenizar (Tokenize)

    Para poder evaluar una expresión, el primer paso será convertirla en una lista de símbolos individuales, o tokens. Esta es la parte más fácil, y no es el tema central de interés en este artículo, así que haremos un poco de trampa para facilitar las cosas.

    En primer lugar, definiremos los tokens (Se observará que falta la definición de números, eso es porque vamos a considerarlos el valor por defecto):

    token_map = {'+':'ADD', '-':'ADD',
                 '*':'MUL', '/':'MUL',
                 '(':'LPAR', ')':'RPAR'}
    Token = namedtuple('Token', ['name', 'value'])
    

    Y este es el código que usaremos para tokenizar una expresión dada, expr:

    split_expr = re.findall('[\d.]+|[%s]' % ''.join(token_map), expr)
    tokens = [Token(token_map.get(x, 'NUM'), x) for x in split_expr]
    

    La primera línea es un truco que divide la expresión en sus componentes básicos, de forma que:

    '1.2 / ( 11+3)' --> ['1.2', '/', '(', '11', '+', '3', ')']
    

    La siguiente línea asigna nombres a los tokens, para que el parser pueda reconocerlos por categoría:

    ['1.2', '/', '(', '11', '+', '3', ')']
    -->
    [Token(name='NUM', value='1.2'), Token(name='MUL', value='/'),
    Token(name='LPAR', value='('), Token(name='NUM', value='11'),
    Token(name='ADD', value='+'), Token(name='NUM', value='3'),
    Token(name='RPAR', value=')')]
    

    Se asume que cualquier token que no esté en el diccionario token_map corresponderá a un número. Nuestro tokenizador carece de la virtud de la validación, que evitaría que se aceptaran cosas que no fueran números, pero por suerte el evaluador realizará más tarde esta tarea por nosotros.

    Ahora que ya tenemos una lista de tokens, nuestro siguiente paso será parsearlo para obtener un AST.

    Paso 2: Definir la gramática

    El parser que usaremos en esta implementación es un versión un tanto ingenua de parser recursivo descendente (recursive descent parser), que es una versión simplificada del parser LL (LL parser). Este tipo de parser es el más fácil de implementar, y de hecho solo nos ocupará unas 14 líneas de código. Es un tipo de parser descendente, lo que significa que primero intenta emparejar la regla más alta (como expression), y continua de forma recursiva intentado emparejar subreglas hasta que alcanza las reglas de más bajo nivel (como por ejemplo, number). Para expresarlo de otra manera, mientras que un parser ascendente (Como un LR Parser o Analizador sintáctico LR en español) intenta de forma gradual "plegar" o "comprimir" tokens y reglas dentro de otras reglas, hasta que solo quede una, un parser descendente irá expandiendo las reglas, usando cada vez reglas menos abstractas, hasta el punto en que haya emparejado completamente los tokens de entrada.

    Pero antes de entrar en materia con el parser, hablemos un poco de la gramática. En el post anterior usamos un parser LR, y se definió la gramática así (las mayúsculas son tokens):

    add: add ADD mul | mul;
    mul: mul MUL atom | atom;
    atom: NUM | '(' add ')' | neg;
    neg: '-' atom;
    

    Note

    Si no entiendes esta gramática, deberías leer el artículo anterior: How To Write A Calculator in 50 Python Lines (Without Eval).

    esta vez usaremos un parser LL, así que definiremos la gramática de esta otra forma:

    rule_map = {
        'add' : ['mul ADD add', 'mul'],
        'mul' : ['atom MUL mul', 'atom'],
        'atom': ['NUM', 'LPAR add RPAR', 'neg'],
        'neg' : ['ADD atom'],
    }
    

    Hay un cambio sutil: Las definiciones recursivas de add y mul están invertidas. Este detalle es muy importante, y necesita su aclaración.

    La versión LR de esta gramática usa lo que se llama recursividad por la izquierda (left recursion en inglés). Cuando un parser LL se encuentra con una llamada recursiva, la ejecuta de inmediato, en un intento de emparejar la regla que está utilizando, por lo que puede entrar en un bucle infinito. Incluso parsers LL bastante avanzados como ANTLR se enfrentan a esta problema, aunque probablemente, en vez de dar vueltas indefinidamente, como hace nuestro parser de juguete, terminarán con un conveniente mensaje de error .

    La recursividad por la izquierda se soluciona de forma sencilla cambiándola por recursividad por la derecha, que es justo lo que acabamos de hacer con nuestro sutil cambio. Pero como nada en la vida es fácil cuando tratamos con parsers, se ha creado otro problema: Mientras que la recursividad por la izquierda analiza 3-2-1 correctamente como (3-2)-1, la recursividad por la derecha lo analiza, de forma incorrecta, como 3-(2-1). No sé como solucionar esto de forma fácil, así que para mantener las cosas sencillas he optado por mantener la forma incorrecta y resolverlo en una etapa posterior (véase el paso 4).

    Paso 3: Obtener un AST (Árbol de Sintaxis Abstracta)

    El algoritmo es sencillo. Vamos a definir una función recursiva que acepta dos parámetros: El primero es el nombre de la regla que estamos intentando capturar, y el segundo es una lista de los tokens que nos quedan por comprobar. Empezaremos con add, -que es la regla más alta- y con la lista de todos los tokens, y haremos que las sucesivas llamadas recursivas sean cada vez más específicas. La función retorna una tupla: La correspondencia encontrada y una lista de los tokens que faltan por comprobar. Con el propósitos de reducir en lo posible el código, este será capaz de encontrar tokens también (En los dos casos son cadenas de texto: en un caso con todas las letras en mayúsculas y en el otro con todas las letras en minúsculas).

    Este es el código del parser:

    RuleMatch = namedtuple('RuleMatch', ['name', 'matched'])
    
    def match(rule_name, tokens):
        if tokens and rule_name == tokens[0].name:      # Match a token?
            return RuleMatch(tokens[0], tokens[1:])
        for expansion in rule_map.get(rule_name, ()):   # Match a rule?
            remaining_tokens = tokens
            matched_subrules = []
            for subrule in expansion.split():
                matched, remaining_tokens = match(subrule, remaining_tokens)
                if not matched:
                    break   # no such luck. next expansion!
                matched_subrules.append(matched)
            else:
                return RuleMatch(rule_name, matched_subrules), remaining_tokens
        return None, None   # match not found
    

    Las líneas 4-5 comprueban si rule_name es realmente un token, y si coincide con el token actual. Si lo hace, devuelve la coincidencia, junto con el resto de los tokens que aun faltan por consumir.

    La línea 6 itera sobre las subreglas de rule_name, de forma que cada una pueda ser analizada recursivamente. Si rule_name es un token, la llamada a get() devolverá una tupla vacía y el código continuará hasta alcanzar el return final, que devuelve una tupla sin valores.

    Las líneas 9-14 iteran sobre cada elemento de la subregla actual, e intenta hacerlas coincidir de forma secuencial. Cada iteración intenta consumir tantos tokens como le sea posible. Si un elemento no coincide, se descarta toda la regla. Si, por el contrario, se consigue casar todos los elementos, alcanzaremos la cláusula else y devolveremos nuestra coincidencia para rule_name, junto con el resto de tokens que faltan por casar.

    Vamos a ejecutarlo con 1.2 / ( 11+3) como entrada:

    >>> tokens = [Token(name='NUM', value='1.2'), Token(name='MUL', value='/'),
                  Token(name='LPAR', value='('), Token (name='NUM', value='11'),
                  Token(name='ADD', value='+'), Token(name='NUM', value='3'),
                  Token(name='RPAR', value=')')]
    >>> match('add', tokens)
    (RuleMatch(name='add', matched=[RuleMatch(name='mul',
    matched=[RuleMatch(name='atom', matched=[Token(name='NUM', value='1.2')]),
    Token(name='MUL', value='/'), RuleMatch(name='mul',
    matched=[RuleMatch(name='atom', matched=[Token(name='LPAR',
    value='('), RuleMatch(name='add', matched=[RuleMatch(name='mul',
    matched=[RuleMatch(name='atom', matched=[Token(name='NUM', value='11')])]),
    Token(name='ADD', value='+'), RuleMatch(name='add',
    matched=[RuleMatch(name='mul', matched=[RuleMatch(name='atom',
    matched=[Token(name='NUM', value='3')])])])]), Token(name='RPAR',
    value=')')])])])]), [])
    

    El resultado es una tupla, como era de esperar, y podemos ver que no queda ningún token para analizar. La coincidencia actual no es fácil de leer, vamos a representarla gráficamente para que se entienda mejor:

    add
        mul
            atom
                NUM '1.2'
            MUL '/'
            mul
                atom
                    LPAR    '('
                    add
                        mul
                            atom
                                NUM '11'
                        ADD '+'
                        add
                            mul
                                atom
                                    NUM '3'
                    RPAR    ')'
    

    Esta es la pinta que presenta un AST, en concepto. Es una buena práctica ejecutar mentalmente el parser, o quizá con la ayuda de papel. Me atrevería a decir que es necesario si realmente quieres entenderlo. Puedes usar este AST como referencia para asegurarte de que lo estás haciendo bien.

    En este punto, henos escrito un parser capaz de analizar correctamente operaciones binarias, operadores unitarios, paréntesis y precedencia,

    Solo hay una cosa que hace mal, y vamos a arreglarla a continuación.

    Paso 4: Post Procesado

    El parser no es perfecto en muchos aspectos. El más importante ahora es que no puede manejar la recursividad por la izquierda, lo que obliga a que la gramática sea recursiva derecha. En consecuencia, analizar la cadena 8/4/2 nos genera el siguiente AST:

    add
        mul
            atom
                NUM 8
            MUL '/'
            mul
                atom
                    NUM 4
                MUL '/'
                mul
                    atom
                        NUM 2
    

    Si intentamos resolver esta expresión con este árbol, tendríamos que calcular primero 4/2, que es incorrecto. Algunos parsers LL eligen arreglar la asociatividad en el árbol. Pero esto llevaría demasiadas líneas ;). En vez de eso, lo que vamos a hacer en "aplanarlo". El método es sencillo: Para cada regla en el AST que:

    1. necesite ser arreglada,
    2. sea una operación binaria (tiene tres subreglas), y
    3. el operando por la derecha resulta ser la misma regla

    entonces "aplanaremos" la última en la primera. Por "aplanar" quiero decir reemplazar un nodo por sus hijos, en el contexto de su padre. Como nuestro recorrido por el árbol es DFS post-order, esto significa que empieza por los bordes del árbol y va progresando hacia la raíz, de forma que los efectos son acumulativos. Este es el código:

    fix_assoc_rules = 'add', 'mul'
    
    def _recurse_tree(tree, func):
        return map(func, tree.matched) if tree.name in rule_map else tree[1]
    
    def flatten_right_associativity(tree):
        new = _recurse_tree(tree, flatten_right_associativity)
        if tree.name in fix_assoc_rules
            and len(new)==3
            and new[2].name==tree.name:
                new[-1:] = new[-1].matched
        return RuleMatch(tree.name, new)
    

    Este código convertirá cualquier composición secuencial de sumas o multiplicaciones en una lista plana (sin mezclar unas con otras). Los paréntesis rompen la secuencia, por supuesta, así que no se ven afectados.

    A partir de aquí se podría reconstruir la estructura en forma asociativa por la izquierda, usando código como:

    def build_left_associativity(tree):
        new_nodes = _recurse_tree(tree, build_left_associativity)
        if tree.name in fix_assoc_rules:
            while len(new_nodes)>3:
                new_nodes[:3] = [RuleMatch(tree.name, new_nodes[:3])]
        return RuleMatch(tree.name, new_nodes)
    

    Pero no lo vamos a hacer. Queremos reducir el número de líneas de código, y cambiar el código de evaluación para que pueda manejar listas conlleva menos líneas que reconstruir el árbol.

    Nota

    DFS post order: DFS significa "búsqueda en profundidad", y significa que a medida que recorremos el árbol, se visitan primero los nodos que están en los niveles inferiores (O, dicho de otra manera, los más profundos). Hay dos tipos de búsquedas en profundidad, en preorden o postorden. La diferencia entra las dos es el momento en que se añade un nodo a la salida, antes o después de visitarlo)

    Paso 5: Evaluar

    Evaluar el árbol es muy sencillo. Lo único que hace falta es navegar por él de forma similar a como lo hicimos en el código de post-proceso (Es decir, DFS post-order) y evaluar cada regla que nos encontramos. Como hemos evaluado primero los nodos más profundos, cada vez que alcanzamos un nodo de tipo regla, sus hijos no pueden ser otra cosa más que números. Este es el código:

    bin_calc_map = {'*':mul, '/':div, '+':add, '-':sub}
    
    def calc_binary(x):
        while len(x) > 1:
            x[:3] = [ bin_calc_map[x[1]](x[0], x[2]) ]
        return x[0]
    
    calc_map = {
        'NUM' : float,
        'atom': lambda x: x[len(x)!=1],
        'neg' : lambda (op,num): (num,-num)[op=='-'],
        'mul' : calc_binary,
        'add' : calc_binary,
    }
    
    def evaluate(tree):
        solutions = _recurse_tree(tree, evaluate)
        return calc_map.get(tree.name, lambda x:x)(solutions)
    

    Hemos escrito calc_binary para que evalúe tanto la suma como la multiplicación (y sus contrapartidas). También evalúa listas, dado el caso, de manera asociativa por la izquierda. De esa forma resolvemos el problema que teníamos con nuestra pequeña gramática LL.

    Paso 6: El REPL (Read–Eval–Print Loop)

    La forma más sencilla posible sería:

    if __name__ == '__main__':
        while True:
            print( calc(raw_input('> ')) )
    

    Que no precisa más explicación. Espero :)

    Apéndice: Todo el código en uno: una calculadora en 70 líneas

    Este es el código final:

    '''A Calculator Implemented With A Top-Down, Recursive-Descent Parser'''
    # Author: Erez Shinan, Dec 2012
    
    import re, collections
    from operator import add,sub,mul,div
    
    Token = collections.namedtuple('Token', ['name', 'value'])
    RuleMatch = collections.namedtuple('RuleMatch', ['name', 'matched'])
    
    token_map = {'+':'ADD', '-':'ADD', '*':'MUL', '/':'MUL', '(':'LPAR', ')':'RPAR'}
    rule_map = {
        'add' : ['mul ADD add', 'mul'],
        'mul' : ['atom MUL mul', 'atom'],
        'atom': ['NUM', 'LPAR add RPAR', 'neg'],
        'neg' : ['ADD atom'],
    }
    fix_assoc_rules = 'add', 'mul'
    
    bin_calc_map = {'*':mul, '/':div, '+':add, '-':sub}
    def calc_binary(x):
        while len(x) > 1:
            x[:3] = [ bin_calc_map[x[1]](x[0], x[2]) ]
        return x[0]
    
    calc_map = {
        'NUM' : float,
        'atom': lambda x: x[len(x)!=1],
        'neg' : lambda (op,num): (num,-num)[op=='-'],
        'mul' : calc_binary,
        'add' : calc_binary,
    }
    
    def match(rule_name, tokens):
        if tokens and rule_name == tokens[0].name:      # Match a token?
            return tokens[0], tokens[1:]
        for expansion in rule_map.get(rule_name, ()):   # Match a rule?
            remaining_tokens = tokens
            matched_subrules = []
            for subrule in expansion.split():
                matched, remaining_tokens = match(subrule, remaining_tokens)
                if not matched:
                    break   # no such luck. next expansion!
                matched_subrules.append(matched)
            else:
                return RuleMatch(rule_name, matched_subrules), remaining_tokens
        return None, None   # match not found
    
    def _recurse_tree(tree, func):
        return map(func, tree.matched) if tree.name in rule_map else tree[1]
    
    def flatten_right_associativity(tree):
        new = _recurse_tree(tree, flatten_right_associativity)
        if tree.name in fix_assoc_rules and len(new)==3 and new[2].name==tree.name:
            new[-1:] = new[-1].matched
        return RuleMatch(tree.name, new)
    
    def evaluate(tree):
        solutions = _recurse_tree(tree, evaluate)
        return calc_map.get(tree.name, lambda x:x)(solutions)
    
    def calc(expr):
        split_expr = re.findall('[\d.]+|[%s]' % ''.join(token_map), expr)
        tokens = [Token(token_map.get(x, 'NUM'), x) for x in split_expr]
        tree = match('add'<div></div>, tokens)[0]
        tree = flatten_right_associativity( tree )
        return evaluate(tree)
    
    if __name__ == '__main__':
        while True:
            print( calc(raw_input('> ')) )
    
    Termina la traducción

    Hasta aquí el artículo original. Si quieres que traduzca el artículo anterior al que se hace referencia en el texto, déjame una nota en los comentarios.

    0

    Añadir un comentario

  5. La revista Full Circle Magazine ha sacado un especial agrupando la serie de artículos sobre programación en Python. Se puede descargar libremente desde aquí:

     

     

    Es la edición número 6, así que supongo que en algún lado estarán las ediciones anteriores, si las encuentro las pondré también. Dicho y hecho:

    Ediciones anteriores

    0

    Añadir un comentario

Archivo del blog
Etiquetas
Etiquetas
Enlaces interesantes
Cargando