Sunday, April 3, 2011

Extendiendo C con Python 2: Retornando vectores

En un post anterior les comenté que se podía ocupar C desde Python de una manera muy chora, dando pie a crear librerías rápidas y eficientes.

La pregunta que quedó en el aire en ese post era cómo pasar vectores creados en C a Python. Aunque dejé un comentario en ese post, quería hacer hincapié en un problema que tuve unos días atrás con la solución que postié allí.

En primera instancia, yo pensé que retornar un vector que llamaremos "theArray" era solamente crear un "objecto Python" (estructura en C) y pasarlo como objeto en Py_BuildValue(), como la respuesta que había dado en ese post. Pero trabajando con dichó código, me di cuenta que había un problema de memoria, por lo que el código que está arriba es incorrecto. Ahora, la pregunta era por qué.

Algunas personas me hicieron ver que en la documentación de la función Py_BuildValue(), sale que esta agrega una referencia al objeto que retorna (en este caso la lista myList). Las referencias son usadas en Python para liberar memoria por una función interna llamada "Garbage Collector". La idea es que si quiero usar algo siempre, "referencio" ese objeto y así no se borra de la memoria, pero si no lo uso lo "derreferencio" y será eventualmente borrado de la memoria (entendiéndose "borrar" como "liberar" ese espacio en la memoria). Esto se hace internamente en varias funciones, pero el tema está en que si algo esta "referenciado", no se borra de la memoria (es como una manera eficiente de guardar y borrar memoria, de modo de no andar haciendo "free()" todo el rato, como en C).

¿La solución? Quitarle la referencia al objeto antes de enviarlo con la función Py_DECREF(). Para ver como era en detalle, vean el post que hize en StackOverflow acá: http://stackoverflow.com/questions/5508904/c-extension-in-python-return-py-buildvalue-memory-leak-problem

Thursday, January 13, 2011

Python (fácil) + C (rápido) = Extendiendo con C


1.- Introducción/Motivación

Es bien sabido que cualquier código de programación semi-interpretado como Python es fácil de usar, pero para iteraciones demasiado grandes, mucha gente alega de que son un poco lentos. Lentos...¿comparados con qué? Bueno, cuando hablamos de lento, siempre se nos viene a la mente el nunca mal ponderado C. C es conocido por ser un lenguaje "básico", pero muy rápido para iterar. Hoy explicaré, más o menos, como unir ámbos: escribir el código en Python, hacer los cálculos con C.

La idea básica (o más bien, como yo lo entendí) es la siguiente: necesitamos crear, primero que todo, un programa en C que podamos llamar desde Python. Por un lado, sabemos que Python trabaja con objetos, mientras que C trabaja con algo similar, pero nunca tan genial, llamadas estructuras. El truco está, justamente, en que C maneje estructuras para manejar nuestros datos, mientras que Python use clases/objetos para manejar los mismos: si hacemos que estos se entiendan de alguna manera, lograremos nuestro objetivo...¡trabajar con ámbos! Organizando un poco más, queremos:

Paso 1: Crear un código en Python.
Paso 2: Enviar datos a C.
Paso 3: Hacer algo con los datos.
Paso 4: Mandar algo de vuelta a Python, desde C.

El Paso 1 lo dejaremos para el final. Asumiendo que de algún modo mandamos datos a C...¡veamos cómo los recibimos!

2.- El Código en C

Supongamos que queremos enviar un vector de datos y un entero que nos da las dimensiones del vector de datos de Python a C y retornar su promedio. Consideren entonces el siguiente código en C que yo llamé "FuncionesDeNes.c":

1 . #include < Python.h >


2 . #include < numpy/arrayobject.h >
3 . #define ARRAYD(p) ((double *) (((PyArrayObject *)p)->data))
4 .
5 . static PyObject *FuncionesDeNes_Promedio(PyObject *self,PyObject *args){
6 .
7 . int i,n_datos;
8 . double promedio,suma=0;
9 . double *vector_en_C;
10. PyObject *vector_de_python;
11. PyArg_ParseTuple(args,"Oi",&vector_de_python,&n_datos);
12. vector_en_C = ARRAYD(vector_de_python);
13.
14. for(i=0;i < n_datos;i++){
15. suma=vector_en_C[i]+suma;
16. }
17.
18. promedio=suma/(double)n_datos;
19.
20. return Py_BuildValue("d",promedio);
21. }
22.
23. static PyMethodDef FuncionesDeNesMethods[] = {
24. {"Promedio", FuncionesDeNes_Promedio, METH_VARARGS, "¿que hace esto?"},
25. {NULL, NULL, 0, NULL}
26. };
27.
28. void initFuncionesDeNes(void){
29. (void) Py_InitModule("FuncionesDeNes", FuncionesDeNesMethods);
30. }



Lo que hace el código de arriba en C es obtener lo que enviamos desde Python (en la función FuncionesDeNes_Promedio) usando el módulo PyArg_ParseTuple, y luego regresa un double con el promedio calculado usando el módulo Py_BuildValue. Es así de simple. Las demás funciones, como se pueden observar, son definiciones para que se pueda "comprender" lo que estamos haciendo a nivel de C.

Nótese lo que sucede en las líneas 10 hasta la 12:

10. PyObject *vector_de_python;
11. PyArg_ParseTuple(args,"Oi",&vector_de_python,&n_datos);
12. vector_en_C = ARRAYD(vector_de_python);
¿Qué hicimos? Lo que tuvimos que definir en la línea 10 fue un puntero tipo PyObject, que es un tipo de estructura definida en el header . Esto define el vector que en Python es un objeto a una estructura en C. Lo que hace justamente PyArg_ParseTuple es recibir la localización de este vector en memorioa (&vector_de_python), asi como también la localización del entero n_datos (&n_datos), que es el número de datos que enviamos desde Python. Fíjense que esta función tiene tres inputs:

11. PyArg_ParseTuple(args,"Oi",&vector_de_python,&n_datos);
El primero (args) son los argumentos que recibe la función. El segundo input ("Oi") lo que hace es decirle a C qué estoy enviando: "O" es para objetos, "i" es para enteros, "d" es para doubles, etc. Si mandara 5 doubles más, por ejemplo, tendría que poner "Oiddddd". Finalmente, los ultimos inputs (&vector_de_python,&n_datos) lo que entregan son los valores a los que asociaremos lo que se envio desde Python: ¡siempre hay que definirlos antes! Ya sean objetos, enteros, doubles, pájaros, ramas, etc. Nótese que el módulo en la línea 20:
20. return Py_BuildValue("d",promedio);
Que es el convertidor desde C a Python, funciona de la misma manera. El paso siguiente es convertir esta estructura a un array en C, para que lo podamos ocupar. Nótese que en la línea 3 hicimos un define de la forma:

3 . #define ARRAYD(p) ((double *) (((PyArrayObject *)p)->data))

Es decir, el módulo ARRAYD(puntero) convierte justamente este puntero PyObject a un array en C para que lo podamos ocupar. La "flecha" lo que hace es asociar un puntero a un miembro de una estructura de la forma:

Puntero -> Miembro de una Estructura

En este caso, apuntamos al miembro "data" que es un miembro de la estructura PyArrayObject (que es lo que justamente estamos enviando, un objeto que es un array en Python: el vector que contiene los datos).

3.- Ok, ¿cómo lo llamo desde Python?
Lo primero que tenemos que hacer es un módulo que contenga este código en C y que desde Python simplemente "compilemos" el programa. Para ello, la solución es hacer un archivo setup.py como el siguiente:

1. from distutils.coreimport setup, Extension
2.
3. module =Extension('FuncionesDeNes', sources = ['FuncionesDeNes.c'])
4. setup(name = 'Funciones creadas por Nestor: Extensiones de C/Python', version = '1.0', ext_modules = [module])

El código es bastante estándar. Hay que tener cuidado con "sources", que debe tener el nombre del archivo .c en el que metimos el código de más arriba. Warning: Hay un pequeño detalle con el código de arriba, eso si. Si eres científico, probablemente trabajarás con gsl, la librería matemática, etc. y por tanto necesitas compilar esas librerías también. Para agregar las librerías a compilar simplemente despues de sources agregamos una sección "libraries" del mismo modo que sources y escribimos las librerías separadas por comas. Por ejemplo, yo uso GSL y la librería matemática en mis códigos (exceptuando en el de más arriba, lógicamente), asi que mi setup.py se ve así:


1. from distutils.coreimport setup, Extension
2.
3. module =Extension('MiModulo', sources = ['MiModulo.c'],libraries=['gsl','gslcblas','m'])
4. setup(name = 'Funciones creadas por Nestor: Extensiones de C/Python', version = '1.0', ext_modules = [module])

De ese modo, al compilar, también se compilaran las librerías del modo "-lgsl -lgslcblas -lm" (nótese que todas las librerías son lnombre, donde "nombre" es el nombre de la librería. Python sabe eso, asi que basta con pasarle "nombre" simplemente: ¡la serpiente se encargará de agregar la "l" y la rayita!). Ahora desde consola, con el código C en el mismo directorio hacemos:

# python setup.py build

Eso creará una carpeta "build" en nuestro directorio, y dentro una carpeta llamada "lib.linux-iVersion-Version2" donde Version es la versión de nuestro SO y Version2 es la versión de Python que ocupamos para compilar. Dentro de la carpeta

"lib.linux-iVersion-Version2" tendremos un archivo .so (en mi caso "FuncionesDeNes.so"). Ese lo movemos simplemente a la carpeta en donde tenemos nuestro archivo en Python desde el cual queremos ocupar el código...¡ahora ocupémos nuestro módulo recién creado!





4.- Ok, ¿cómo lo llamo desde Python? (¿ahora si?)







Ok, ok, tranquilos. La paciencia es la clave del éxito (?). Consideren el siguiente código en Python:





1. # -*- coding: utf-8 -*-
2. from numpy.randomimport *
3. importFuncionesDeNes
4.
5. Datos=randn(10000000,1)
6. printFuncionesDeNes.Promedio(Datos,len(Datos))

Hay tres cosas claves en este código. Primero...¿qué hace randn(10000000,1)? Es simplemente una función de numpy.random que genera 10000000 (10^7) números, todos con una distribución normal estándar (media 0, desviación estándar 1) en un arreglo numpy. Así, Datos[0] entrega el primer número, Datos[1] el segundo, etc. Obsérvese que en la línea 3 simplemente importamos el módulo y en la línea 6 lo usamos, pasándole los inputs que habíamos dicho...¡y eso es todo!




5.- Ok, se ve lindo y todo pero...¿es necesario?








¡Es la misma pregunta que me hize yo!, ¿realmente valdrá la pena? Pues los invito a realizar el mismo código en Python (es decir, hacer la iteración de los 10^7 números con un "for" desde Python). En mi caso, desde mi notebook, Python tomó entre 55 y 57 segundos en entregarme el valor del promedio...por otro lado, ocupando este método, C se demoró entre 0.04 y 0.05 segundos: IN-CRE-ÍBLE, pero cierto. Inténtenlo, se van a sorprender.

¡Saludos!