Tag Archive: python


Bucles rápidos en Python

Como algunos habréis notado, siento cierta afición por Python. Ésto es así porque programar en Python es (por lo general) mucho más divertido que programar con la mayoría de lenguajes existentes. Aún así tengo ciertos problemas de conciencia al respecto, Python es un lenguaje interpretado y eso se nota en el rendimiento de los programas que se hacen con él. Yo recomendaría Python para algunas cosas muy concretas: aprendizaje, prototipado, usarlo como “lenguaje pegamento” para unir programas o librerías hechas en otros lenguajes más eficientes, o por último, como lenguaje de scripting del sistema (mucho mejor que Bash o Csh).

Aun así, puede darse el caso de que nos dé la gana de hacer una aplicación de uso común en la que sería deseable que el rendimiento fuera bueno, lo que no será posible si no conocemos algunos detalles de Python. Hoy comentaré como hacer bucles lo más rápidos posible.

En principio tenemos 2 formas de crear bucles en Python, con el bucle for y con el bucle while. El bucle for ejecuta un bloque de código por cada elemento de un objeto iterable.

L = [1 2 3 4]
for i in L:
  accion(i)

Mientras que el bucle while ejecuta un trozo de código mientras se cumpla una condición concreta:

i = 1
while i <= 4:
  accion(i)
  i += 1

Los objetos iterables pueden ser de muchos tipos, pueden ser listas (que consumen memoria por cada uno de sus elementos) o pueden ser también generadores, objetos que devuelven un elemento nuevo cada vez que se llama a su método __iter__, también se pueden crear métodos generadores que actúan como objetos iterables. Así que podemos crear un método que hará algo parecido a la función range:

def generador(n):
  i = 0
  while i < n:
    yield i
    i += 1

Que podemos usar en un bucle:

  for i in generador(5):
    print i

Nota: por si alguien no sabe lo que hace la función range, crea una lista de 0 a n y la devuelve (donde n es el parámetro que se le pasa).

¿Qué ventajas tienen los generadores sobre la función range? Pues para empezar, no consumen tanta memoria, no hace falta crear un array de n elementos para hacer un bucle con n iteraciones. Aun así, si experimentamos con nuestros generadores y comparamos tiempos de ejecución veremos que la función range funciona significativamente más rápida. La razón es que está programada internamente en C y nuestro generador está programado en Python.

Ésto nos puede hacer pensar que la mejor alternativa, entonces, es usar un bucle while (aunque sea más feo) ya que evitaremos la sobrecarga de llamar a una función, no gastaremos tanta memoria como con range y sólo haremos una comparación al principio y una suma al final de cada iteración. Pero... nos equivocaríamos otra vez. Resulta que la función range da mejores resultados que el típico bucle while en cuanto a tiempo, la razón, otra vez, es que está programada internamente en C. De hecho es esperable que si el array creado por range fuera lo suficientemente grande, los resultados cambiaran (porque se tendría que recurrir a la memoria virtual y habría fallos de página), pero para que eso se dé hacen falta muuchas iteraciones, sobretodo ahora, cuando tenemos tanta memoria RAM.

Después de todo, podemos acabar un poco desanimados, pues no hemos visto ninguna solución que nos aporte todo lo que queremos. Pero está ahí :) , y se llama xrange. Funciona de manera análoga a la función range, sólo que en vez de devolvernos un array lo que hace es devolvernos un generador sobre el que podrá iterar el bucle for de forma elegante y aprovechando el rendimiento de C (pues está programada internamente en C). Así pues, la manera más eficiente de hacer un bucle viene a ser algo como ésto:

for i in xrange(5):
  print i

No pongo aquí los tiempos porque haría demasiado largo el artículo y lo podéis calcular vosotros mismos de forma fácil con el comando time de UNIX. Saludos!

Python Bytecode Disassembler ( dis )

El artículo que sigue es una traducción de un artículo escrito en la página web Python Module of the Week, que es una especie de recopilatorio de artículos sobre módulos de Python escritos por Doug Hellman. Sus textos se publican bajo la licencia Creative Commons By-Nc-Sa ( como los míos, por si alguien no lo había notado todavía con el logo de la página ).

Python Bytecode Disassembler ( dis )

El módulo que trataremos se llama dis y su principal utilidad es convertir código objeto a una representación de bytecode que sea entendible para los seres humanos (o almenos para aquellos que hayan perdido un poco de su tiempo en intentar entender éstas cosas). Éste texto está indicado para versiones de Python iguales o superiores a la versión 1.4, por lo que no tendréis ningún problema (ahora todo el casi mundo usa versiones iguales o superiores a la 2.4).

El módulo dis incluye funciones para desensamblar bytecode de Python (que se genera durante la interpretación del código para acelerar el funcionamiento de los scripts).Observar el código bytecode ejecutado por el intérprete es una buena forma de optimizar a mano bucles y otras secuencias de código. También es útil para encontrar condiciones de carrera en aplicaciones multihilo ya que mirando el bytecode se puede ver en qué “momento” es más probable que haya un cambio de hilo.

Desensamblado básico

La función dis.dis() muestra por pantalla la representación del desensamblado de código fuente Python (módulo, clase, método, función, o código objeto). Podemos desensamblar código como el siguiente:

#!/usr/bin/env python
# encoding: utf-8

my_dict = { 'a':1 }

ejecutando dis desde la línea de comandos. La salida está organizada en columnas con el número de línea original del código fuente, la “dirección” dentro del código objeto, el nombre de opcode y los argumentos pasados al opcode.

$ python -m dis codigo.py
  4           0 BUILD_MAP                1
              3 LOAD_CONST               0 (1)
              6 LOAD_CONST               1 ('a')
              9 STORE_MAP
             10 STORE_NAME               0 (my_dict)
             13 LOAD_CONST               2 (None)
             16 RETURN_VALUE

En este caso el código se traduce a 5 operaciones para inicializar el diccionario (crearlo y llenarlo), luego guarda los resultados en una variable global. Como el intérprete de Python está basado en un esquema de pila, los primeros pasos consisten en poner las constantes en la pila siguiendo el orden correcto con la operación LOAD_CONST, y luego usar STORE_MAP para sacar la clave y el valor que se añadirán al diccionario (no nos olvidemos de que antes se ha hecho la operación BUILD_MAP, los valores añadidos entre la ejecución de BUILD_MAP y STORE_MAP serán las claves y los valores del diccionario que estamos creando). El objeto resultante se enlaza con el nombre “my_dict” con la operación STORE_NAME.

Desensamblando funciones

Desafortunadamente desensamblar el módulo entero no lo hace con las funciones que hay en él automáticamente. Por ejemplo, si desensamblamos éste módulo:

#!/usr/bin/env python
# encoding: utf-8

def f(*args):
    nargs = len(args)
    print nargs, args

if __name__ == '__main__':
    import dis
    dis.dis(f)

los resultados muestran como se carga el código objeto en la pila y luego se salta dentro de la función (LOAD_CONST, MAKE_FUNCTION), pero el cuerpo de la función no está.

$ python -m dis dis_function.py
  4           0 LOAD_CONST               0 ()
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)

  8           9 LOAD_NAME                1 (__name__)
             12 LOAD_CONST               1 ('__main__')
             15 COMPARE_OP               2 (==)
             18 JUMP_IF_FALSE           29 (to 50)
             21 POP_TOP

  9          22 LOAD_CONST               2 (-1)
             25 LOAD_CONST               3 (None)
             28 IMPORT_NAME              2 (dis)
             31 STORE_NAME               2 (dis)

 10          34 LOAD_NAME                2 (dis)
             37 LOAD_ATTR                2 (dis)
             40 LOAD_NAME                0 (f)
             43 CALL_FUNCTION            1
             46 POP_TOP
             47 JUMP_FORWARD             1 (to 51)
        >>   50 POP_TOP
        >>   51 LOAD_CONST               3 (None)
             54 RETURN_VALUE

Para ver dentro de la función tenemos que pasarla como argumento a dis.dis().

$ python dis_function.py
  5           0 LOAD_GLOBAL              0 (len)
              3 LOAD_FAST                0 (args)
              6 CALL_FUNCTION            1
              9 STORE_FAST               1 (nargs)

  6          12 LOAD_FAST                1 (nargs)
             15 PRINT_ITEM
             16 LOAD_FAST                0 (args)
             19 PRINT_ITEM
             20 PRINT_NEWLINE
             21 LOAD_CONST               0 (None)
             24 RETURN_VALUE

Clases

Se pueden pasar también clases a la función dis, en este caso todos sus métodos son desensamblados a la vez.

#!/usr/bin/env python
# encoding: utf-8

import dis

class MyObject(object):
    """Example for dis."""

    CLASS_ATTRIBUTE = 'some value'

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return 'MyObject(%s)' % self.name

dis.dis(MyObject)
$ python dis_class.py
Disassembly of __init__:
 12           0 LOAD_FAST                1 (name)
              3 LOAD_FAST                0 (self)
              6 STORE_ATTR               0 (name)
              9 LOAD_CONST               0 (None)
             12 RETURN_VALUE

Disassembly of __str__:
 15           0 LOAD_CONST               1 ('MyObject(%s)')
              3 LOAD_FAST                0 (self)
              6 LOAD_ATTR                0 (name)
              9 BINARY_MODULO
             10 RETURN_VALUE

Desensamblando para debuggear

A veces puede ser útil ver qué bytecode causó el problema cuando se está debuggeando una excepción.  Hay un par de formas de desensamblar el código que encierra el error.

La primera forma consiste en usar dis.dis() dentro del intérprete interactivo para que analize la última excepción ocurrida. Si no se le pasa ningún argumento a dis, ésta busca la última excepción ocurrida y muestra el desensamblado de la parte “más alta” de la pila que la causó.

$ python
Python 2.6.2 (r262:71600, Apr 16 2009, 09:17:39)
[GCC 4.0.1 (Apple Computer, Inc. build 5250)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> j = 4
>>> i = i + 4
Traceback (most recent call last):
  File "", line 1, in
NameError: name 'i' is not defined
>>> dis.distb()
  1 -->       0 LOAD_NAME                0 (i)
              3 LOAD_CONST               0 (4)
              6 BINARY_ADD
              7 STORE_NAME               0 (i)
             10 LOAD_CONST               1 (None)
             13 RETURN_VALUE
>>>

Notad la flecha --> indicando el opcode que causó el error. La variable “i” no está definida, por lo que el valor asociado con el nombre no puede ser cargado en la pila.

Desde tu propio código puedes mostrar por pantalla información sobre el traceback pasándolodirectamente como argumento a dis.distb(). En este ejemplo hay una excepción DivideByZero, pero como la fórmula tiene dos partes, no está claro cual de los elementos es el cero.

#!/usr/bin/env python
# encoding: utf-8

i = 1
j = 0
k = 3

# ... many lines removed ...

try:
    result = k * (i / j) + (i / k)
except:
    import dis
    import sys
    exc_type, exc_value, exc_tb = sys.exc_info()
    dis.distb(exc_tb)

El valor incorrecto es fácil de detectar cuando está cargado en la pila dentro del desensamblado. La operación incorrecta está remarcada con la flecha -->, y sólo tenemos que mirar unas cuantas líneas hacia arriba para encontrar dónde se ha cargado el valor 0 en la pila.

$ python dis_traceback.py
  4           0 LOAD_CONST               0 (1)
              3 STORE_NAME               0 (i)

  5           6 LOAD_CONST               1 (0)
              9 STORE_NAME               1 (j)

  6          12 LOAD_CONST               2 (3)
             15 STORE_NAME               2 (k)

 10          18 SETUP_EXCEPT            26 (to 47)

 11          21 LOAD_NAME                2 (k)
             24 LOAD_NAME                0 (i)
             27 LOAD_NAME                1 (j)
    -->      30 BINARY_DIVIDE
             31 BINARY_MULTIPLY
             32 LOAD_NAME                0 (i)
             35 LOAD_NAME                2 (k)
             38 BINARY_DIVIDE
             39 BINARY_ADD
             40 STORE_NAME               3 (result)
             43 POP_BLOCK
             44 JUMP_FORWARD            65 (to 112)

 12     >>   47 POP_TOP
             48 POP_TOP
             49 POP_TOP

 13          50 LOAD_CONST               3 (-1)
             53 LOAD_CONST               4 (None)
             56 IMPORT_NAME              4 (dis)
             59 STORE_NAME               4 (dis)

 14          62 LOAD_CONST               3 (-1)
             65 LOAD_CONST               4 (None)
             68 IMPORT_NAME              5 (sys)
             71 STORE_NAME               5 (sys)

 15          74 LOAD_NAME                5 (sys)
             77 LOAD_ATTR                6 (exc_info)
             80 CALL_FUNCTION            0
             83 UNPACK_SEQUENCE          3
             86 STORE_NAME               7 (exc_type)
             89 STORE_NAME               8 (exc_value)
             92 STORE_NAME               9 (exc_tb)

 16          95 LOAD_NAME                4 (dis)
             98 LOAD_ATTR               10 (distb)
            101 LOAD_NAME                9 (exc_tb)
            104 CALL_FUNCTION            1
            107 POP_TOP
            108 JUMP_FORWARD             1 (to 112)
            111 END_FINALLY
        >>  112 LOAD_CONST               4 (None)
            115 RETURN_VALUE

Análisis de rendimiento en bucles

Además de para localizar errores, dis también puede ayudar a encontrar problemas de rendimiento en nuestro código. Examinar el código desensamblado es especialmente útil con pequeños bucles en los que el número de líneas de código Python es pequeño pero éstas se  ejecutan lentamente ya que se traducen a un conjunto ineficiente de bytecodes. Veremos como el desensamblado nos ayuda a examinar unas pocas implementaciones de una clase, Dictionary, que lee un conjunto de palabras y las agrupa por su primera letra.

Antes de nada, la aplicación que usaremos para hacer los tests:

import dis
import sys
import timeit

module_name = sys.argv[1]
module = __import__(module_name)
Dictionary = module.Dictionary

dis.dis(Dictionary.load_data)
print
t = timeit.Timer(
    'd = Dictionary(words)',
    """from %(module_name)s import Dictionary
words = [l.strip() for l in open('/usr/share/dict/words', 'rt')]
    """ % locals()
    )
iterations = 10
print 'TIME: %0.4f' % (t.timeit(iterations)/iterations)

Podemos usar dis_test_loop.py para ejecutar cada versión de la clase Dictionary que hagamos.

Una implementación sencilla de la classe Dictionary puede ser algo así:

#!/usr/bin/env python
# encoding: utf-8

class Dictionary(object):

    def __init__(self, words):
        self.by_letter = {}
        self.load_data(words)

    def load_data(self, words):
        for word in words:
            try:
                self.by_letter[word[0]].append(word)
            except KeyError:
                self.by_letter[word[0]] = [word]

La salida muestra que esta versión ha tomado 0.1074 segundos para cargar las 234936 palabras en mi copia de  /usr/share/dict/words en OS X [Recordad que es una traducción y no lo hice directamente yo ésto]. No está demasiado mal, pero como podemos ver en el desensamblado de abajo, el bucle. está haciendo más trabajo del necesario. Tal como entra en el bucle en el opcode 13, se instala el contexto de una excepción (SETUP_EXCEPT). Entonces usa 6 opcodes para encontrar self.by_letter[word[0]] antes de añadir la palabra a la lista. Si se lanza una excepción porque word[0] todavía no está en el diccionario, el manejador de excepciones hace otra vez el mismo trabajo para  determinar word[0] (3 opcodes) y inicializa self.by_letter[word[0]] como una nueva lista que contiene la palabra.

$ python dis_test_loop.py dis_slow_loop
 11           0 SETUP_LOOP              84 (to 87)
              3 LOAD_FAST                1 (words)
              6 GET_ITER
        >>    7 FOR_ITER                76 (to 86)
             10 STORE_FAST               2 (word)

 12          13 SETUP_EXCEPT            28 (to 44)

 13          16 LOAD_FAST                0 (self)
             19 LOAD_ATTR                0 (by_letter)
             22 LOAD_FAST                2 (word)
             25 LOAD_CONST               1 (0)
             28 BINARY_SUBSCR
             29 BINARY_SUBSCR
             30 LOAD_ATTR                1 (append)
             33 LOAD_FAST                2 (word)
             36 CALL_FUNCTION            1
             39 POP_TOP
             40 POP_BLOCK
             41 JUMP_ABSOLUTE            7

 14     >>   44 DUP_TOP
             45 LOAD_GLOBAL              2 (KeyError)
             48 COMPARE_OP              10 (exception match)
             51 JUMP_IF_FALSE           27 (to 81)
             54 POP_TOP
             55 POP_TOP
             56 POP_TOP
             57 POP_TOP

 15          58 LOAD_FAST                2 (word)
             61 BUILD_LIST               1
             64 LOAD_FAST                0 (self)
             67 LOAD_ATTR                0 (by_letter)
             70 LOAD_FAST                2 (word)
             73 LOAD_CONST               1 (0)
             76 BINARY_SUBSCR
             77 STORE_SUBSCR
             78 JUMP_ABSOLUTE            7
        >>   81 POP_TOP
             82 END_FINALLY
             83 JUMP_ABSOLUTE            7
        >>   86 POP_BLOCK
        >>   87 LOAD_CONST               0 (None)
             90 RETURN_VALUE

TIME: 0.1074

Una técnica para elimnar la excepción es rellenar self.by_letter con una lista para cada letra del alfabeto antes de empezar a llenar el diccionario. Esto significa que siempre podremos hacer la operación append satisfactoriamente sin necesidad de manejar ninguna excepción.

#!/usr/bin/env python
# encoding: utf-8

import string

class Dictionary(object):

    def __init__(self, words):
        self.by_letter = dict( (letter, [])
                                for letter in string.letters)
        self.load_data(words)

    def load_data(self, words):
        for word in words:
            self.by_letter[word[0]].append(word)

El cambio reduce el número de opcodes aproximadamente a la mitad, pero solo se reduce el tiempo a 0.0984 segundos. Obviamente el manejo de la excepción añadía un cierto overhead pero tampoco demasiado.

$ python dis_test_loop.py dis_faster_loop
 14           0 SETUP_LOOP              38 (to 41)
              3 LOAD_FAST                1 (words)
              6 GET_ITER
        >>    7 FOR_ITER                30 (to 40)
             10 STORE_FAST               2 (word)

 15          13 LOAD_FAST                0 (self)
             16 LOAD_ATTR                0 (by_letter)
             19 LOAD_FAST                2 (word)
             22 LOAD_CONST               1 (0)
             25 BINARY_SUBSCR
             26 BINARY_SUBSCR
             27 LOAD_ATTR                1 (append)
             30 LOAD_FAST                2 (word)
             33 CALL_FUNCTION            1
             36 POP_TOP
             37 JUMP_ABSOLUTE            7
        >>   40 POP_BLOCK
        >>   41 LOAD_CONST               0 (None)
             44 RETURN_VALUE

TIME: 0.0984

Podemos optimizar aún más el rendimiento moviendo el acceso a self.by_letter fuera del bucle (dado que el valor no cambia en ningún momento).

#!/usr/bin/env python
# encoding: utf-8

import collections

class Dictionary(object):

    def __init__(self, words):
        self.by_letter = collections.defaultdict(list)
        self.load_data(words)

    def load_data(self, words):
        by_letter = self.by_letter
        for word in words:
            by_letter[word[0]].append(word)

Los opcodes 0-6 ahora encuentran el valor de self.by_letter y lo guardan como la variable local by_letter. Usar variables locales solo requiere un opcode en vez de 2 (en la posición 22 se usa LOAD_FAST para almacenar dictionary en la pila). Después de este cambio el tiempo de ejecución se reduce a 0.0842 segundos.

$ python dis_test_loop.py dis_fastest_loop
 13           0 LOAD_FAST                0 (self)
              3 LOAD_ATTR                0 (by_letter)
              6 STORE_FAST               2 (by_letter)

 14           9 SETUP_LOOP              35 (to 47)
             12 LOAD_FAST                1 (words)
             15 GET_ITER
        >>   16 FOR_ITER                27 (to 46)
             19 STORE_FAST               3 (word)

 15          22 LOAD_FAST                2 (by_letter)
             25 LOAD_FAST                3 (word)
             28 LOAD_CONST               1 (0)
             31 BINARY_SUBSCR
             32 BINARY_SUBSCR
             33 LOAD_ATTR                1 (append)
             36 LOAD_FAST                3 (word)
             39 CALL_FUNCTION            1
             42 POP_TOP
             43 JUMP_ABSOLUTE           16
        >>   46 POP_BLOCK
        >>   47 LOAD_CONST               0 (None)
             50 RETURN_VALUE

TIME: 0.0842

Una mayor optimización sugerida por Brandon Rhodes es eliminar la versión Python del bucle por completo. Si usamos groupby() del módulo itertools para agrupar la entrada, la iteración es movida a código C. Podemos hacer ésto porque sabemos que la entrada está ordenada. En caso de no saber si está ordenada o no, deberíamos ordenarla por si acaso.

#!/usr/bin/env python
# encoding: utf-8

import operator
import itertools

class Dictionary(object):

    def __init__(self, words):
        self.by_letter = {}
        self.load_data(words)

    def load_data(self, words):
        # Arrange by letter
        grouped = itertools.groupby(words, key=operator.itemgetter(0))
        # Save arranged sets of words
        self.by_letter = dict((group[0][0], group) for group in grouped)

La versión con itertools solo tarda 0.0543 segundos en ejecutarse, más o menos la mitad del tiempo original.

$ python dis_test_loop.py dis_eliminate_loop
 15           0 LOAD_GLOBAL              0 (itertools)
              3 LOAD_ATTR                1 (groupby)
              6 LOAD_FAST                1 (words)
              9 LOAD_CONST               1 ('key')
             12 LOAD_GLOBAL              2 (operator)
             15 LOAD_ATTR                3 (itemgetter)
             18 LOAD_CONST               2 (0)
             21 CALL_FUNCTION            1
             24 CALL_FUNCTION          257
             27 STORE_FAST               2 (grouped)

 17          30 LOAD_GLOBAL              4 (dict)
             33 LOAD_CONST               3 ( at 0x7e7b8, file "/Users/dhellmann/Documents/PyMOTW/dis/PyMOTW/dis/dis_eliminate_loop.py", line 17>)
             36 MAKE_FUNCTION            0
             39 LOAD_FAST                2 (grouped)
             42 GET_ITER
             43 CALL_FUNCTION            1
             46 CALL_FUNCTION            1
             49 LOAD_FAST                0 (self)
             52 STORE_ATTR               5 (by_letter)
             55 LOAD_CONST               0 (None)
             58 RETURN_VALUE

TIME: 0.0543

Referencias:

$ python dis_test_loop.py dis_eliminate_loop
 15           0 LOAD_GLOBAL              0 (itertools)
              3 LOAD_ATTR                1 (groupby)
              6 LOAD_FAST                1 (words)
              9 LOAD_CONST               1 ('key')
             12 LOAD_GLOBAL              2 (operator)
             15 LOAD_ATTR                3 (itemgetter)
             18 LOAD_CONST               2 (0)
             21 CALL_FUNCTION            1
             24 CALL_FUNCTION          257
             27 STORE_FAST               2 (grouped)

 17          30 LOAD_GLOBAL              4 (dict)
             33 LOAD_CONST               3 (<code object <genexpr> at 0x7e7b8, file "/Users/dhellmann/Documents/PyMOTW/dis/PyMOTW/dis/dis_eliminate_loop.py", line 17>)
             36 MAKE_FUNCTION            0
             39 LOAD_FAST                2 (grouped)
             42 GET_ITER
             43 CALL_FUNCTION            1
             46 CALL_FUNCTION            1
             49 LOAD_FAST                0 (self)
             52 STORE_ATTR               5 (by_letter)
             55 LOAD_CONST               0 (None)
             58 RETURN_VALUE

TIME: 0.0543

Novedades en CMD Twitt

No hay mucho que decir pero ha habido algunos avances significativos desde que escribí el primer post. He añadido algunas opciones más para que el programa sea más útil, he corregido dos pequeños bugs relativos a la gestión de errores y he simplificado la gestión de la codificación de texto gracias a una característica de la librería python-twitter que me había pasado desapercibida. También he creado alguna función más que me ha permitido simplificar un poco más el código.

Lo que me queda por hacer:

  1. Añadir un modo interactivo, lo programaré con la librería curses
  2. Aprovechar la capacidad de internacionalización del programa y traducirlo al castellano y al catalán
  3. Empaquetarlo para Debian
  4. Si a alguien se le ocurre algo más, tiene todo el derecho del mundo a decirlo :) .

Algunas cosillas más :) :

Añadí el proyecto a Launchpad ( CMD Twitt ) ya que gracias a ésto ahora tengo un bug tracker donde todo el mundo puede indicar los bugs que encuentre en el programa :) y también dejar sugerencias de mejora en los Blue Prints.

Saludos!

Cmd Twitt

Hace tres días empecé a programar Cmd Twitt (y lo acabé ayer, bueno, supongo que todavía se puede mejorar pero no le voy a dedicar mucho más tiempo a no ser que se reporten bugs alguien tenga buenas ideas para mejorarlo). Éste programa es un simple programa de consola que permite acceder a los servicios de la popular red social Twitter.

Lo programé en Python usando la librería python-twitter, que se puede instalar mediante aptitude o apt-get en sistemas Debian o Ubuntu. Hay algunos detalles interesantes del programa sobre los que me gustaría escribir porque aprendí algunas cosillas mientras lo estaba haciendo, entre otras cosas las siguientes:

  • Internacionalización de aplicaciones Python
  • Correcto funcionamiento con diferentes codificaciones de texto
  • Gestión sencilla de ficheros de configuración
  • Poner colorines en la consola, jeje
  • Esto tiene poco que ver con la programación en sí, pero es interesante también: Trabajar con git y gitorious

Os cuelgo el código y acto seguido vamos con la explicación:

#!/usr/bin/env python
# encoding: utf-8
# -*- coding: utf-8 -*-

############################################################################
#                     --- cmd-twitt 2009.08.16 ---                         #
#                                                                          #
#   Copyright (C) 2009 by Andreu Correa Casablanca  (Original Author)      #
#                                                                          #
#   Email: castarco@gmail.com (Andreu Correa Casablanca)                   #
#                                                                          #
#   This program is free software; you can redistribute it and/or modify   #
#   it under the terms of the GNU General Public License as published by   #
#   the Free Software Foundation; either version 3 of the License, or      #
#   (at your option) any later version.                                    #
#                                                                          #
#   This program is distributed in the hope that it will be useful,        #
#   but WITHOUT ANY WARRANTY; without even the implied warranty of         #
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          #
#   GNU General Public License for more details.                           #
#                                                                          #
#   You should have received a copy of the GNU General Public License      #
#   along with this program; if not, write to the                          #
#   Free Software Foundation, Inc.,                                        #
#   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.              #
############################################################################

import ConfigParser
import getpass
import gettext
import locale
import optparse
import os
import sys
import twitter
import urllib2

# An alias that allows us to imitate the touch command
touch = lambda file: open(file, "w").close()

cmd_usage   = 'usage: %prog [option] [num_of_twits_per_timeline]'
cmd_version = '2009.08.16'

conf_dir  = os.environ["HOME"] + "/.cmdtwitt/"
conf_file = conf_dir + "user.conf"

encoding  = locale.getpreferredencoding().lower()

user = ''
pswd = ''

color = {}
color['green']   = '\033[1;32m'
color['nocolor'] = '\033[1;0m'
color['red']     = '\033[1;31m'
color['white']   = '\033[1;37m'
color['yellow']  = '\033[1;33m'

# Translate some global vars
def init_vars():
        global cmd_usage

        cmd_usage   = _(cmd_usage)

# Load the account settings
def load_config():
        global user, pswd

        cfg = ConfigParser.ConfigParser()

        try:
                cfg.readfp(file(conf_file))
        except:
              raise

        user = cfg.get ('TWIT_ACCOUNT', 'User')
        pswd = cfg.get ('TWIT_ACCOUNT', 'Pswd')

#Set the account settings
def set_config():
        global conf_dir, conf_file

        print _("Settings:")

        cfg = ConfigParser.ConfigParser()
        cfg.add_section('TWIT_ACCOUNT')

        cfg.set('TWIT_ACCOUNT', 'User', raw_input(_('\tUser: ')) )

        pswd_not_set = True
        while pswd_not_set:
                pswd1 = getpass.getpass(_('\tPassword       : '))
                pswd2 = getpass.getpass(_('\tRepeat password: '))

                if pswd1 == pswd2:
                        pswd_not_set = False
                else:
                        print _('The passwords don\'t match. Please try to write it again.')

        cfg.set('TWIT_ACCOUNT', 'Pswd', pswd1)

        if os.access(conf_file, os.F_OK) == False:
                try:
                        touch(conf_file)
                except IOError:
                        try:
                                os.mkdir(conf_dir, 0700)
                        except IOError:
                                print _('It was impossible to create the config dir.')
                                raise

                        try:
                                touch(conf_file)
                        except:
                                print _('It was impossible to create the config file.')
                                raise

        try:
                cfg.write(file(conf_file, 'w'))
        except:
                raise

# Manage connection errors
def manage_connection_error(e, code):
        if e.getcode() == code:
                print _('\tUnauthorized acces, you should set a correct username and password.')
        else:
                print _('It seems twitter is down. Try it later.')

# Sends a message to a user
def send_msg_to_user(_user):
        global user, pswd, cmd_version

        api = twitter.Api(username=user, password=pswd)

        msg = unicode(raw_input( _('Type your message:\n\t') ), encoding)[:140]

        try:
                api.PostDirectMessage(_user, msg)

        except urllib2.HTTPError, e:
                print _('Error sending the message:')

                manage_connection_error(e, 401)

# Show the user status
def show_status():
        global user, pswd

        api = twitter.Api(username=user, password=pswd)

        try:
                status = api.GetUserTimeline(user)[0].text

                print _('Your status is:\n\t' + status)
        except urllib2.HTTPError, e:
                print _('Error reading your status:')

                manage_connection_error(e, 404)

# Format the timeline to show in the console
def show_timeline(num_msgs, statuses):
        global color

        _status = statuses[0]
        print color['green']+_status.user.name + ' :\n' + color['yellow'] + _status.GetRelativeCreatedAt() + ' > ' + color['nocolor'] + _status.text + '\n'

        for status in statuses[1:num_msgs]:
                if status.user.name == _status.user.name:
                        name = color['yellow'] + status.GetRelativeCreatedAt()+' > ' + color['nocolor']
                else:
                        name = color['green']+status.user.name + ' :\n' + color['yellow'] + status.GetRelativeCreatedAt() + ' > ' + color['nocolor']

                print name + status.text + '\n'
                _status = status

# Shows the timeline of a specific twitter user
def show_usertimeline(_user, num_msgs):
        global user, pswd, color

        api = twitter.Api(username=user, password=pswd)

        try:
                statuses = api.GetUserTimeline(_user)

                print color['red'] + _user + _(' Timeline:\n') + color['nocolor']

                show_timeline(num_msgs, statuses)

        except urllib2.HTTPError, e:
                print _('Error reading the timeline:')

                manage_connection_error(e, 401)

# Shows the Home timeline
def show_friendstimeline(num_msgs):
        global user, pswd, color

        api = twitter.Api(username=user, password=pswd)

        try:
                statuses = api.GetFriendsTimeline(user)

                print color['red'] +_('Home Timeline:\n') + color['nocolor']

                show_timeline(num_msgs, statuses)

        except urllib2.HTTPError, e:
                print _('Error reading the timeline:')

                manage_connection_error(e, 401)

# Shows the Home timeline
def show_publictimeline(num_msgs):
        global user, pswd

        api = twitter.Api(username=user, password=pswd)

        try:
                statuses = api.GetPublicTimeline()

                print color['red'] + _('Public Timeline:\n') + color['nocolor']

                show_timeline(num_msgs, statuses)

        except urllib2.HTTPError, e:
                print _('Error reading the timeline:')

                manage_connection_error(e, 401)

# Send a message to twitter
def send_msg(_user):
        global user, pswd, cmd_version

        api = twitter.Api(username=user, password=pswd)

        try:
                if _user != '':
                        _user = '@'+_user+' '

                api.PostUpdate( ( _user + unicode(raw_input( _('Write your message and press Enter:\n\t') ), encoding) )[:140] )

        except urllib2.HTTPError, e:
                print _('Error sending the message:')

                manage_connection_error(e, 401)

# The main program
def main(argv=None):
        global user, pswd

        if argv == None:
                argv = sys.argv

        # Internationalization
        gettext.install('cmd-twitt')

        init_vars()

        cmd_parser = optparse.OptionParser(usage=cmd_usage, version=cmd_version, conflict_handler='resolve')

        cmd_parser.add_option('-h', '--help',    action='help',    help=_('print this help text and exit'))
        cmd_parser.add_option('-v', '--version', action='version', help=_('print program version and exit'))

        cmd_parser.add_option('-c', '--config', dest='config', action='store_true', help=_('Configure your twitter account'))
        cmd_parser.add_option('-s', '--status', dest='status', action='store_true', help=_('Shows your status'))

        cmd_parser.add_option('-p', '--publictimeline',  dest='publictimeline',  action='store_true', help=_('Shows the public timeline'))
        cmd_parser.add_option('-u', '--usertimeline',    dest='usertimeline',    action='store_true', help=_('Shows the user timeline'))
        cmd_parser.add_option('-F', '--friendtimeline',  dest='friendtimeline',  metavar='FRIEND',    help=_('Shows the timeline of a friend'))
        cmd_parser.add_option('-f', '--friendstimeline', dest='friendstimeline', action='store_true', help=_('Shows the friends timeline'))

        cmd_parser.add_option('-m', '--private-message', dest='private_msg', metavar='USER', help=_('Sends a private message to a twitter user'))
        cmd_parser.add_option('-M', '--public-message',  dest='public_msg',  metavar='USER', help=_('Sends a public message to a twitter user'))

        (cmd_opts, cmd_args) = cmd_parser.parse_args()

        if cmd_opts.config:
                try:
                        set_config()
                except:
                        print _("It was impossible to set the settings.")
                        return 1
        else:
                try:
                        load_config()
                except:
                        print _('There is not a config file, you should set your user and password in the settings option.\n')
                        cmd_parser.print_help()
                        return 1

                try:
                        num_msgs = int(cmd_args[0])
                except:
                        num_msgs = 10

                if cmd_opts.private_msg:
                        send_msg_to_user(cmd_opts.private_msg)
                elif cmd_opts.public_msg:
                        send_msg(cmd_opts.public_msg)
                elif cmd_opts.status:
                        show_status()
                elif cmd_opts.usertimeline:
                        show_usertimeline(user, num_msgs)
                elif cmd_opts.friendtimeline:
                        show_usertimeline(cmd_opts.friendtimeline, num_msgs)
                elif cmd_opts.friendstimeline:
                        show_friendstimeline(num_msgs)
                elif cmd_opts.publictimeline:
                        show_publictimeline (num_msgs)
                else:
                        send_msg('')

if __name__ == "__main__":
        sys.exit(main())

Internacionalización

Lo primero que se tiene que hacer es escribir la linia import gettext para cargar el módulo que nos ayudará en nuestro quehacer. La segunda línea (no del texto, sino referente a la internacionalización, la podemos encontrar en la función main) gettext.install('cmd-twitt') se dedica a cargar la traducción al idioma por defecto del sistema para las cadenas de texto que usa el programa. La traducción se cargará desde el directorio /usr/share/locale/[abreviatura para el lenguaje por defecto del sistema]/LC_MESSAGES/cmd-twitt.mo.

Por último (en cuanto se refiere al código) lo que se tiene que hacer para que todas las cadenas que queramos sean susceptibles de traducción a través del módulo gettext es escribirlas de ésta forma _('cadena'). La función _() es un alias para gettext.gettext().

Quedan otros pasos que no tienen mucho que ver con el código, sinó con la traducción en sí. Para empezar a traducir el programa lo primero que debemos hacer es obtener un fichero especial sobre el que se basarán las traducciones. Lo haremos así:

gettext cmd-twitt.py

Ésto generará un fichero llamado messages.po que contendrá parejas de cadenas, las originales junto con sus traducciones. Para empezar la traducción al castellano generamos un nuevo fichero a partir de messages.po que será el que contendrá la traducción en sí (messages.po lo guardamos como base para traducciones a otros lenguajes). Lo haremos como se sigue:

msginit --locale=es -i messages.po

Éste programa nos pedirá algunos datos acerca de nosotros (para que quede constancia de quienes son los traductores,  está bien que se sepa de quien es la autoría) y finalmente creará un fichero llamado es.po , que es el que nos dedicaremos a modificar.

Finalmente el fichero puede ser editado por herramientas tales como gtranslator o ktranslator para luego ser compilado a un fichero con extensión .mo que será el que irá al directorio de traducciones que mencionamos anteriormente. (Cambiando su nombre es.mo a cmd-twitt.mo)

Codificaciones de texto

Éste es un tema que ha dado mucho por saco, la verdad sea dicha. De hecho la librería python-twitter no funciona demasiado bien en este aspecto, falta que la pulan un poco (y no solo por lo de la codificación de texto, yo ya he tenido que corregir algunos bugs en la versión de mi sistema, tengo que ver ahora si es la versión de Ubuntu o la del repositorio oficial de código tiene los mismos errores).

Percibí el problema en cuanto intenté enviar mensajes con acentos a través de Cmd Twitt a Twitter, ¿Cual era el problema? El programa reventaba. Mi primer logro consistió en hacer que funcionara para mi sistema, pero luego ajusté un poco mejor el funcionamiento.. y CREO (no estoy del todo seguro) que funcionará en sistemas con diferentes codificaciones de texto.

Primera solución:

Supongamos que str es una cadena que hemos obtenido a través de la función raw_input() , la solución consistía en usar la cadena resultantde de aplicar la función unicode() a str, de la siguiente forma: unicode(str, 'utf-8') . Si no se añade el segundo argumento la función unicode() intenta leer str como si fuera ASCII y el programa revienta igualmente (que es justo lo que se hace en la librería python-twitter, un error un poco burdo, se nota que no deben hablar otras lenguas los creadores de la librería).

Ésta solución funciona en mi sistema porque usa UTF-8 para la codificación de las cadenas… pero eso no es necesariamente así en todos los sistemas.

Segunda solución:

La segunda solución pasa por el uso del módulo locale, así que tenemos que hacer un import de éste. Simplemente tenemos que sustituir la cadena ‘utf-8′ por el resultado de locale.getpreferredencoding().lower() .

Ficheros de configuración

No me extenderé mucho con ésto. He usado el módulo ConfigParser , y en cuanto a la introducción de la contraseña de usuario para twitter he utilizado un módulo llamado getpass que permite introducir caracteres sin tener que mostrarlos por pantalla. És fácil de aprender viendo el código como ejemplo.

Colorines

Lo reconozco, hay que estar un poco zumbado para dedicar una sección entera de la explicación ssólo para los colorines, qué le vamos a hacer, soy así. Aunque se reduce a algo tan simple como añadir al principio de la cadena que queremos colorear ciertas cadenas características que harán cambiar el color del texto de la consola (incluso después de haberse cerrado el programa). Ésto solo funciona para algunas consolas concretas.. no las he provado, pero aseguro que funciona con Bash, y también aseguro que no funciona en MS Windows.

Las cadenas son de la forma ‘\033[1;32m’ en la que para escoger el color sólo se tienen que cambiar los dos números del final, ésta en particular es para el color verde. Al final del artículo encontraréis un enlace para ver la tabla de códigos y sus colores correspondientes.

Git y Gitorious

Me da pereza seguir explicándolo todo como si fuera un cuento, voy al grano:

  • Crear una cuenta en gitorious.org
  • Subir una clave pública de ssh a Gitorious, si ya existe en vuestro sistema la tendréis en el fichero /home/usuario/.ssh/id_rsa.pub . En caso de no tenerla, lo podéis hacer mediante el comando ssh-keygen -t rsa .
  • Crear un proyecto y un repositorio, es todo guiado así que resulta muy sencillo.
  • Ahora toca la parte de configurar el repositorio en nuestro ordenador de trabajo. Tendremos que tener instalados ssh y git antes que nada. Los pasos que seguiremos son:
    • cd directorio_proyecto
    • git init  # Creamos el repositorio local
    • git add . # Ésto añade el directorio a la lista de ficheros del repositorio git
    • git commit -a -m "Y se hizo la luz"  # El comentario no es realmente importante (almenos en el primer commit, vamos)
    • git remote add origin git@gitorious.org:miproyecto/mainline.git # Esto nos servirá para "conectar" nuestro repositorio local con el remoto de Gitorious.org, en la documentación que encontré ponían origen en vez de origin pero luego el siguiente paso no se podía hacer de la manera "corta" que seguidamente indico
    • git push origin master # Con ésto subimos los cambios que hemos hecho al repositorio remoto, las próximas veces sólo tendremos que escribir git push

Ahora os dejo las fuentes que me han servido para informarme un poco:

Internacionalización:

Ficheros de configuración en Python:

Colorines:

Configuración de Git y Gitorious:

Hasta otra :) .

Por el momento no he sido capaz de conseguir ejecutar scripts Python sobre el servidor web Cherokee de la misma forma que se hace habitualmente con PHP, y creo que lo tengo peludo si realmente quiero eso. Como mucho he conseguido hacer GCIs que funcionan como eso, como CGIs, pero eso no se parece en nada a lo que yo quiero, pues no me ofrece todo el dinamismo necesario.

Por otro lado, conseguí hacer alguna aplicación de prueba en Python que ejecuta su propio servidor web con WSGI (en particular con una librería de Python que implementa wsgi), todo bastante fácil:

#!/usr/bin/python

def handle_request(environment, start_response):
    start_response('200 OK', [('content-type', 'text/html')])
    return ['Hello, World!']

if __name__ == '__main__':
    from wsgiref import simple_server
    simple_server.make_server('', 8080, handle_request).serve_forever()

Pero esta aplicación funciona independientemente de cualquier servidor web, y sinceramente, no creo que sea capaz de soportar una gran carga de conexiones, ni que sea excesivamente rápida, por no hablar de la flexibilidad en la gestión del servidor.

Por eso seguí el tutorial de la página web del proyecto Cherokee para cargar Django en el servidor (y también seguí el de Trac, que es básicamente hacer lo mismo), pero cambiando algunas cosas… como por ejemplo, no ejecutar ni Django ni Trac, sino la aplicación que he escrito arriba (que está copiada de un tutorial de esos que se encuentran perdidos por la red). Este modelo es algo más interesante que lo que se hace usualmente con PHP, pues se tiene una aplicación ya cargada en memoria y corriendo en paralelo con el servidor web, sirviendole las vistas cuando éste se las pide. Pero… no sé por qué razón, no lo he conseguido, haciendo un “ps -A” en la shell veo como Cherokee ha cargado mi aplicación (que llamé serv.py, Cherokee carga la aplicación segun los comandos que se le indican si detecta que no hay respuesta en el puerto indicado en la configuración). Cuando arrancaba yo la aplicación manualmente podía acceder directamente al puerto especificado, y si Cherokee intentaba acceder, la aplicación también reaccionaba, aunque no como me hubiera gustado (la modifiqué ligeramente para que hiciera unos logs, pero nada, me parece que saltaban excepciones por todas partes :s ).

Aun así, despues de todo, no estoy tan mal, las pruebas con Django me fueron de maravilla y aunque puede añadir un poco de sobrecarga a mi aplicación, almenos funciona, y por otro lado acelerará el desarrollo.

Bien, llevo unos días intentando configurar un servidor web Cherokee 0.noventa_y_algo para poder ejecutar aplicaciones web hechas con Python (igual que la mayoría de la gente las hace en PHP), para ello tengo que saber (creo) configurar el SCGI .. y creo que hay poco más. En teoría debe ser sencillo, pero no lo consigo, ¿Alguien ha conseguido configurar Cherokee+Python? Tengo que decir que no le he dedicado excesivo tiempo… pero es que la universidad (y mis relaciones sociales) también requieren su tiempo.

Bueno, agradeceré a cualquiera que me pueda dar una pista :) hasta otra!

Solucionados bugs graves en oggtube

He solucionado dos bugs graves que conocía de hace tiempo en oggtube (tengo que decir que los descubrí tardíamente por la falta de testers.. aunque bueno, dada la pésima calidad del programa, es normal que nadie se preste a probarlo, xD).

El primer fallo (el más gordo) tenía que ver con la descarga de múltiples vídeos con una sola llamada al programa.. no encadenaba bién las urls.. por lo que sólo era capaz de bajar correctamente el primer vídeo de la lista. La solución: he aplicado un map, que conozco gracias a la asignatura ‘Teoría de la programació’ que imparten en la UAB… aunque no la enseñaran para Python, supuse que existiría y acerté.

Nota sobre esta solución: Creo que a partir de la versión 3 de Python la función map dejará de existir en el lenguaje para pasar a implementarse al estilo de los lenguajes funcionales, tengo que reconocer que he visto códgo y queda mucho más bonito.. por lo general, por que no siempre es así. (En la versión 2.6 todavía guardan compatibilidad hacia atrás)

El segundo fallo era “menor”, simplemente he añadido un aviso para cuando se intenta ejecutar el modo gráfico sin que estén las librerías Qt4 y sus bindigs para Python instaladas en el sistema. (De otra forma el programa se moría dejando mensajes muy feos que no aportaban mucha información al usuario).

El código se sigue pudiendo bajar desde el svn de lafarga.cat … aunque tampoco creo que sea de gran interés… (últimamente he visto que han surgido muchos programas que hacen lo mismo que el mío.. pero de forma más bonita, lo tengo que reconocer, ellos se lo curraron y yo no).

Bueno, hasta otra.

El otro día me planteé la cuestión de cómo crear un módulo para Python que fuera más eficiente que el propio Python, es decir, sin escribirlo en Python. Me puse a mirar la documentación oficial y lo encontré fácilmente, la verdad es que Python está muy bien documentado (cambiaría algunas cosillas, pero más por gustos personales que por que crea que están mal). La principal forma de crear módulos eficientes para Python es hacerlos en C o C++, y ésta es la forma que seguí yo. Antes de continuar, os preguntaréis… ¿Por qué querías hacer un módulo eficiente para Python? Pues… ni idea, jeje, por experimentar, y por ver si con mis conocimientos recién adquiridos (sí, realmente recién adquiridos) de métodos numéricos soy capaz de hacer una buena librería de cálculo científico (en particular para aplicar técnicas de interpolación) para Python. Sólo he visto la PyGMP, pero es posible que sea demasiado general, y no incluye algoritmos de interpolación (que es, en definitiva, lo que yo quiero hacer).

Por ahora sólo he incluido dos algoritmos muy sencillos, el de Neville para evaluar polinomios en un punto (de momento sólo real), y el método de las diferencias divididas de Newton, que nos da las diferencias divididas con las que podemos construir un polinomio interpolatorio. De momento he hecho sólo éstos dos porque he estado haciendo pruebas, y entre ellas han habido pruebas de rendimiento… la verdad es que me quedé pasmado al ver la diferencia entre C y Python (sí, ya sabía que Python era más lento, pero no tanto). El algoritmo de Neville llega a ser un 862% más rápido escrito en C que en Python (tarda sólo un 11.5% de lo que tardaría si estuviera escrito en Python), además, aunque no lo he comprobado, creo que a medida que aumentamos los puntos de soporte, el margen de tiempo aumenta todavía más (supongo que habrá un límite).

Ahora pasaré a explicar brevemente como lo he hecho, aunque para el que quiera profundizar, tendría que leer la documentación de Python directamente, esto debería ser sólo un ejemplo. Lo primero que debemos hacer es crear el código que queremos transformar en un módulo python (ya desde el principio tenemos que empezar utilizando los tipos de la API de Python):

#include


/* Neville interpolation on real value */
static PyObject *interpolation_iaval(PyObject *self, PyObject *args)
{
	double x;
	PyObject *a, *fa;
	PyObject *tmp;

	int n;
	unsigned i, j;

	double *da, *dfa;

	if (!PyArg_ParseTuple(args, "dOO", &x, &a, &fa))
		return PyExc_AttributeError;
	if(!(PyList_Check(a) || PyList_Check(fa)))
		return PyExc_AttributeError;
	n = PyList_Size(a);
	if(n!=PyList_Size(fa))
		return PyExc_IndexError;

	da  = (double *)malloc(n*sizeof(double));
	dfa = (double *)malloc(n*sizeof(double));

	if(da==NULL || dfa==NULL)
	{
		if(da)  free(da);
		if(dfa) free(dfa);
		return PyErr_NoMemory();
	}

	for(i=0; i=i; j--)
			dfa[j] = ((x-da[j-i])*dfa[j]-(x-da[j])*dfa[j-1])/(da[j]-da[j-i]);

	x = dfa[n];

	free(da);
	free(dfa);

	return Py_BuildValue("d", x);
}

static PyObject *interpolation_divdif(PyObject *self, PyObject *args)
{
	PyObject *a, *fa;
	PyObject *tmp;

	int n;
	unsigned i, j;

	double *da, *dfa;

	if (!PyArg_ParseTuple(args, "OO", &a, &fa))
		return PyExc_AttributeError;
	if(!(PyList_Check(a) || PyList_Check(fa)))
		return PyExc_AttributeError;
	n = PyList_Size(a);
	if(n!=PyList_Size(fa))
		return PyExc_IndexError;

	da  = (double *)malloc(n*sizeof(double));
	dfa = (double *)malloc(n*sizeof(double));

	if(da==NULL || dfa==NULL)
	{
		if(da)  free(da);
		if(dfa) free(dfa);
		return PyErr_NoMemory();
	}

	for(i=0; i=i; j--)
			dfa[j] = (dfa[j]-dfa[j-1])/(da[j]-da[j-i]);
	n++;
	tmp = PyList_New((Py_ssize_t)(n));
	for(i=0; i

Una vez escrito el código tendría que explicarlo... pero en serio, da pereza explicarlo, :p, mirad la referencia de Python online http://docs.python.org/ext/ext.html (Extendiendo Python, en inglés), http://docs.python.org/api/api.html (Python/C API Reference Manual). Después de entender lo que hemos escrito, tenemos que hacer que sea accesible desde el intérprete Python. Desde al versión 2.3 tenemos un método muy sencillo. Este se basa en crear una especie de "instalador" en un pequeño archivo Python que cuando es interpretado construye e instala el módulo para que pueda ser utilizado sin ningún problema.

from distutils.core import setup, Extension

module1 = Extension('interpolation',
                    sources = ['interpolationmodule.c'])

setup (name = 'interpolation',
       version = '0.1',
       description = 'Interpolation Package',
       ext_modules = [module1])

Por cierto, el fichero C lo llamé interpolationmodule.c, y a éste fichero Python lo llamaremos setup.py. Para construir el módulo escribimos python setup.py build . Para instalarlo debemos entrar en modo administrador y escribir lo siguiente: python setup.py install . Y con esto finaliza la instalación del módulo. Si queréis probarlo sólo hace falta escribir import interpolation en el intérprete, las funciones disponibles son iaval (método de Neville) y divdif (método de las diferencias divididas). Para los que queráis probar las diferecias de rendimiento, podéis importar el módulo timeit, que se utiliza como sigue.

Asignamos a una variable un objeto Timer:
t = timeit.Timer("interpolation.iaval(3.0, [2.0, 4.0], [5.0, 6.0])", "import interpolation")
Luego ejecutamos la función timeit() del objeto que hemos creado. Ésta función lo que hará será llamar a la función que le hemos indicado un millón de veces dentro del entorno que le indicamos en el segundo parámetro. Una vez acabe su trabajo, nos indicará el tiempo transcurrido. Si creamos una función en Python que haga el mismo trabajo y le damos los mismos datos, veremos con timeit la gran diferencia de rendimiento que hay entre ambas.

Bueno, acabo este artículo nada reflexivo, aunque espero, entretenido para frikis como yo.

Oggtube ya tiene alojamiento :)

Antes de empezar mi sesión de estudio de la asignatura ‘Mètodes Numèrics’ (una de las asignaturas que más miedo me dan, aunque el profesor que la imparte es de los que más aprecio tienen por mi parte, básicamente un tío extraordinario) tengo que anunciar una cosa:

Oggtube ya tiene un alojamiento en https://projectes.lafarga.cat/projects/oggtube . He corregido un bug que permitía bajar diferentes vídeos guardándolos con el mismo nombre de fichero (con lo que unos podían sobreescribir a otros) y ya he creado una interfaz gráfica que se puede ver con la opción -g (aunque todavía no hace nada de nada).

Podéis bajar las últimas versiones del proyecto desde el repositorio con el siguiente comando:
svn checkout --username anonymous https://svn.projectes.lafarga.cat/svn/oggtube

Bueno, ahora ya toca estudiar :) .

Más versiones

Espero que el blog no se transforme simplemente en el lugar donde cuelgo el código que voy haciendo, aunque esta vez lo voy a volver a hacer (no tengo mucho tiempo para escribir largas reflexiones porque estoy de exámenes). El código lo he escrito en el tren después de haber suspendido con casi toda seguridad el exámen de Análisis en varias variables de esta tarde, básicamente lo he hecho para relajarme un poco :p .

Changelog: corregidos “bugs” que no corregí en el otro por no haber testeado el código (nombres mal escritos de algunas variables), he mejorado ligeramente el código utilizando la librería ‘shutils’ y exprimiendo más la librería ‘os’, de forma que ahora el código es multiplataforma en toda regla. (Haría falta ver si en Windows existe el programa ffmpeg2theora, jeje. Por otro lado, es ligeramente más tosco el funcionamiento en Windows que en Linux porque no utilizo ningún directorio temporal para guardar los ficheros intermedios). Ahí va el código:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

############################################################################
#                      --- oggtube 2006.06.10 ---                          #
#                                                                          #
#   Copyright (C) 2007 by Julià Mestieri Ferrer  (Original Author),        #
#                         Andreu Correa Casablanca (Adaptation).           #
#                                                                          #
#   Email: castarco@gmail.com (Andreu Correa Casablanca)                   #
#                                                                          #
#   This program is free software; you can redistribute it and/or modify   #
#   it under the terms of the GNU General Public License as published by   #
#   the Free Software Foundation; either version 2 of the License, or      #
#   (at your option) any later version.                                    #
#                                                                          #
#   This program is distributed in the hope that it will be useful,        #
#   but WITHOUT ANY WARRANTY; without even the implied warranty of         #
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          #
#   GNU General Public License for more details.                           #
#                                                                          #
#   You should have received a copy of the GNU General Public License      #
#   along with this program; if not, write to the                          #
#   Free Software Foundation, Inc.,                                        #
#   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.              #
############################################################################

import os
import optparse
import shutil
import sys

youtubedl_call = "youtube-dl http://www.youtube.com/v/"

cmdl_usage = 'usage: %prog [options] videocode_1,vc_2,...,vc_n'
cmdl_version = '2008.06.04'
cmdl_parser = optparse.OptionParser(usage=cmdl_usage, version=cmdl_version, conflict_handler='resolve')
cmdl_parser.add_option('-h', '--help', action='help', help='print this help text and exit')
cmdl_parser.add_option('-v', '--version', action='version', help='print program version and exit')
cmdl_parser.add_option('-u', '--username', dest='username', metavar='USERNAME', help='account username')
cmdl_parser.add_option('-p', '--password', dest='password', metavar='PASSWORD', help='account password')
cmdl_parser.add_option('-o', '--output', dest='outfile', metavar='FILE', help='output video file names "name1,name2,..."')
(cmdl_opts, cmdl_args) = cmdl_parser.parse_args()

if len(cmdl_args) != 1:
	cmdl_parser.print_help()
	sys.exit('\n')

codes = cmdl_args[0].split(',')

if cmdl_opts.outfile:
	filenames = cmdl_opts.outfile.split(',')
else:
	filenames = []

for x in range(len(filenames)):
	if filenames[x][-4:] != ".ogg":
		filenames[x] += ".ogg"

for x in range(len(filenames), len(codes)):
	filenames.append(codes[x]+".ogg")

if cmdl_opts.username:
	youtubedl_call += " -u "+cmdl_opts.username

if cmdl_opts.password:
	youtubedl_call += " -p "+cmdl_opts.password

x = 0
while x < len(codes):
	if os.access(filenames[x], os.F_OK):
		print('The file '+filenames[x]+' already exists, try with another file name.\n')
		filenames.pop(x)
		codes.pop(x)
	else:
		x += 1

if os.name == 'posix':
	dir = os.getcwd()
	os.chdir('/tmp')
	for x in range(len(codes)):
		if filenames[x][0] != '/':
			filenames[x] = dir+"/"+filenames[x]

if len(codes) > 0:
	if os.system(youtubedl_call+" ".join(codes)) !=0:
		sys.exit("I can't download the videos you specified.\n")
else:
	sys.exit('There is not any file that i can download.\n')

for x in range(len(codes)):
	os.system("ffmpeg2theora "+codes[x]+".flv")
	shutil.move(codes[x]+".ogg", filenames[x])
	os.remove(codes[x]+".flv")

sys.exit()
Powered by WordPress | Theme: Motion by 85ideas.