Scrivere una semplice estensione C per Python
medio
Obiettivo
Scrivere una semplice estensione in C richiamabile da Python.
Prerequisiti
Conoscenza almeno basilare dei linguaggi C e Python.
Iniziamo!
In questa breve guida andremo a vedere come scrivere una semplice estensione in C richiamabile da Python.
Integrare C in Python può essere utile essenzialmente in due casi:
- quando vi sono determinate porzioni di codice per cui è richiesta una maggiore velocità di esecuzione
- quando ci si vuole interfacciare a chiamate del sistema operativo che non sono disponibili nativamente in Python
In questa breve guida andremo a prendere in esame il primo caso, ovvero appoggiarci ad un'estensione scritta in C per velocizzare il tempo di esecuzione di un determinato algoritmo.
Versioni a confronto: Python e C
Prendiamo come esempio una funzione che calcoli il fattoriale di un numero compreso tra 1 e 10.
In Python:
#!/usr/bin/env python
# factorial.py
def factorial(n):
if (n < 1) or (n > 10):
raise ValueError("Specifica un numero tra 1 e 10")
fact = 1
while n > 1:
fact *= n
n -= 1
return fact
if __name__ == '__main__':
print "Il fattoriale di 10 e': %s" %factorial(10)
In C:
//_factorial.c
#include <stdlib.h>
int factorial(int n)
{
int fact = 1;
if ((n < 1) || (n > 10)){
printf("Specifica un numero tra 1 e 10");
return -1;
}
while (n > 1){
fact = fact * n;
n = n - 1;
}
return fact;
}
int main()
{
printf("Il fattoriale di 10 e': %d\n", factorial(10));
return 1;
}
- Nota: per compilare la versione C è sufficiente utilizzare il comando "gcc _factorial.c" che genererà un eseguibile chiamato "a.out"
Si può notare sin da subito la differenza di velocità delle due implementazioni calcolando il tempo di esecuzione tramite il comando time.
Utilizzando time possiamo notare come la versione C sia circa 8 volte più veloce di quella Python.
user@ubuntu:~/foo$ time python pyfactorial.py Il fattoriale di 10 e': 3628800 real 0m0.049s user 0m0.030s sys 0m0.020s user@ubuntu:~/foo$ time ./a.out Il fattoriale di 10 e': 3628800 real 0m0.006s user 0m0.000s sys 0m0.010s user@ubuntu:~/foo$
Integrare il codice C in Python
Abbiamo visto le due versioni dello stesso programma implementate nei due diversi linguaggi.
Quello che vogliamo fare ora è modificare la versione C affinchè sia richiamabile da Python come se fosse un comune modulo avente estensione .py.
Per fare ciò andremo ad eseguire una serie di mofiche sul file _cfactorial.c che, senza scendere troppo nel dettaglio (c'è l'apposita documentazione per questo), si possono riassumere in:
- dichiarare una routine dei nomi tramite PyMethodDef
- dichiarare una routine di inizializzazione
- modificare la funzione C scritta in precedenza affinchè accetti un oggetto Python di tipo int come argomento (tramite PyArg_ParseTuple) e ne ritorni un altro dello stesso tipo (tramite Py_BuildValue)
Il codice:
// _factorial.c
#include <Python.h>
#include <stdlib.h>
static PyObject* factorial(PyObject* self, PyObject* args);
// name routine
static PyMethodDef module_methods[] = {
{"factorial", factorial, METH_VARARGS, "Docstring"},
{NULL, NULL, 0, NULL}
};
// initializer
PyMODINIT_FUNC
init_factorial(void)
{
Py_InitModule3("_factorial", module_methods, "doc");
}
static PyObject*
factorial(PyObject* self, PyObject* args)
{
int n;
int fact = 1;
/*
args rappresenta la tupla di argomenti passata dal codice Python.
Assumo che vi sia un solo argomento, mi aspetto che sia di tipo int,
e lo salvo nella variabile "n" tramite "PyArg_ParseTuple".
*/
if (! PyArg_ParseTuple(args, "i", &n)) {
return PyErr_Format(PyExc_ValueError, "Invalid argument");
}
if ((n < 1) || (n > 10)){
return PyErr_Format(PyExc_ValueError, "Specifica un numero tra 1 e 10");
}
while (n > 1){
fact = fact * n;
n = n - 1;
}
/*
Py_BuildValue ritorna un tipo Python. In questo caso utilizzando
l'argomento "i" specifico un tipo intero.
*/
return Py_BuildValue("i", fact);
}
Terminate le modifiche sul codice C non ci resta che scrivere uno script setup.py che, utilizzando distutils, compili il codice C e lo trasformi in un modulo binario avente estensione .so (.pyd su Windows) importabile dall'interprete Python.
#!/usr/bin/env python
# setup.py
from distutils.core import setup, Extension
setup(name='factorial',
ext_modules=[Extension('_factorial', ['_factorial.c'])],
)
Ora che l'estensione è pronta è sufficiente installarla tramite il classico "python setup.py install":
user@ubuntu:~/foo$ sudo python setup.py install running install running build running build_ext building '_factorial' extension gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/usr/local/include/python2.6 -c _factorial.c -o build/temp.linux-i686-2.6/_factorial.o gcc -pthread -shared build/temp.linux-i686-2.6/_factorial.o -o build/lib.linux-i686-2.6/_factorial.so running install_lib copying build/lib.linux-i686-2.6/_factorial.so -> /usr/local/lib/python2.6/site-packages running install_egg_info Removing /usr/local/lib/python2.6/site-packages/cfactorial-0.0.0-py2.6.egg-info Writing /usr/local/lib/python2.6/site-packages/cfactorial-0.0.0-py2.6.egg-info user@ubuntu:~/foo$
Non rimane che lanciare l'interprete Python e utilizzare l'estensione C dall'interprete Python:
user@ubuntu:~/foo$ python
Python 2.6.1 (r261:67515, Mar 3 2009, 14:11:52)
[GCC 4.3.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import _factorial
>>> _factorial.factorial(10)
3628800
>>> _factorial.factorial(1)
1
>>> _factorial.factorial("string")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: Invalid argument
>>>
Nota finale
Le operazioni descritte in questa guida possono essere velocizzate di molto utilizzando Swig.
Link utili
- Extending and Embedding the Python Interpreter: http://docs.python.org/extending/index.html
- Writing C extension modules with distutils: http://docs.python.org/distutils/setupscript.html#describing-extension-modules