https://imgs.xkcd.com/comics/efficiency.png

Python bindings#

Qué son#

Python es un lenguaje con muchas virtudes. Por ejemplo, es fácil comenzar a ser productivo con unas mínimas nociones y resulta muy adecuado para el desarrollo de proyectos en colaboración. Además con Python cualquier usuario tiene acceso a una enorme cantidad de herramientas, librerías, que facilitarán su trabajo casi en cualquier ámbito. Pero si algo no es Python -al menos hasta la versión 3.10- es rápido. Python es un interpretador de código que prescinde del paso de traducir el código a código máquina para su ejecución. Esto, que puede resultar ventajoso en muchas circunstancias, es una desventaja cuando se trata de realizar trabajo computacional pesado. Es por eso que resulta muy habitual implementar las partes del código más demandantes en un segundo lenguaje, como C o Fortran, para compilar esas funciones para ser invocadas desde el código Python. A las herramientas que hacen de puente entre Python y un lenguaje compilado se les llama «enlaces a Python» o Python «bindings».

Existen varios «enlaces a Python» y siguen implementándose nuevos mecanísmos y librerías para este propósito. Aquí repasaremos brevemente tres opciones para que quien se acerca por primera vez a ellos disponga un poquito de información para entender qué ventajas le pueden aportar.

¿Necesito entonces aprender otro lenguaje de programación?#

Aprender otros lenguajes de programación puede ser muy buena idea si tienes el tiempo para hacerlo. Según tus necesidades e intereses, si ya conoces Python, vas a jugar con ciertas ventajas si además conoces otros lenguajes de programación interpretado como R, Ruby, JavaScript o Ruby. Pero sin duda, te complementará mucho conocer un lenguaje compilado como Rust, C, C++ o Fortran. ¿Es estricamente necesario para poder producir herramientas computacionales que puedan ser ejecutadas en tiempos mucho más cortos que con Python? No, puedes conocer únicamente Python y disponer de funciones compiladas sin necesidad de cambiar de lenguaje de programación. Si este es tu caso, existen herramientas como Cython o Numba que te resultarán extremádamente útiles -échale un ojo a la unidad de esta sesión dedicada a Numba-.

ctypes#

El primer mecanismo para enlazar código escrito y compilado en C desde Python que debe ser mencionado en esta unidad es sin duda «ctypes». Puede que no lo supieras pero Python está programado completamente en C. Por este motivo C es el lenguaje compilado natural al que cualquiera recurriría en primer lugar para hacer su código más rápido. «ctypes» es la herramienta nativa incluida en Python para comunicarse con módulos compilados en C.

Cómo se instala#

Para hacer uso de «ctypes» no debes instalar nada más que Python. «ctypes» forma parte de las librerías esenciales de Python.

Un ejemplo#

Para tener un punto de referencia en el caso del uso de «ctypes» y los posteriores mecanismos de Python Binding, hagamos en primer lugar un algoritmo sencillo en Python con el que poder comparar.

Veamos un algoritmo para ordenar números enteros (conocido con el nombre «Bubble Sort») y observemos cuanto tiempo tarda en ejecutarse cuando la cantidad de números es \(10^5\):

import numpy as np

def sort(serie):
    
    n_numbers = serie.shape[0]
    
    done = False
    
    while not done:
        done = True
        for ii in range(n_numbers-1):
            if serie[ii]>serie[ii+1]:
                serie[ii], serie[ii+1] = serie[ii+1], serie[ii]
                done = False
    
    pass

Vamos a generar aleatoriamente \(10^{5}\) números enteros comprendidos en el rango \([0,10^6)\) y tomemos el tiempo de ejecución para futuras referencias:

rng = np.random.default_rng()
serie = rng.integers(0, 10**5, 10**4)
backup_serie = serie.copy()
%%time
sort(serie)
CPU times: user 27.4 s, sys: 23.1 ms, total: 27.4 s
Wall time: 27.4 s
serie
array([   17,    25,    39, ..., 99962, 99986, 99999])

Ya tenemos un punto de referencia. Vamos a ver cómo podemos hacer uso del mismo algoritmo escrito y compilado en C desde Python con «ctypes».

Supongamos que tenemos el siguiente código en C que hace esencialmente lo mismo que nuestra rutina anterior:

#include  <stdio.h>

void sort(int serie[], int size) {

  int done = 0;
    
  while (done==0) {
      
    int done = 1;

    for (int ii = 0; ii < size - 1; ++ii) {
      if (serie[ii] > serie[ii + 1]) {
        int temp = serie[ii];
        serie[ii] = serie[ii + 1];
        serie[ii + 1] = temp;
        
        done = 0;
      }
    }
    
  }
}

El fichero lo puedes encontrar en el mismo directorio de esta unidad en su repositorio.

En primer lugar necesitamos compilar el código para generar una librería estática que llamaremos sort.so. Usaremos el compilador libre gcc:

!gcc -fPIC -Wall -shared -o sort.so sort.c

El símbolo exclamación nos permite ejecutar en el sistema operativo que aloja ejecuta este Jupyter Notebook órdenes cómo si estuvieramos haciendo uso de una terminal.

Como puedes comprobar, la librería estática ya se encuentra en este directorio:

!ls
python_bindings.ipynb  sort.c  sort.so

Para poder invocar desde Python la función compilada en C únicamente tenemos que importar la librería nativa «ctypes» para hacer los siguiente:

from numpy.ctypeslib import ndpointer
import ctypes

clib = ctypes.cdll.LoadLibrary("./sort.so")
serie = backup_serie.copy()
output = np.zeros((serie.shape[0]), dtype=serie.dtype)
clib.sort.restype = None
%%time
clib.sort(ctypes.c_void_p(serie.ctypes.data), ctypes.c_int(serie.shape[0]), ctypes.c_void_p(output.ctypes.data))
CPU times: user 179 ms, sys: 1 µs, total: 179 ms
Wall time: 179 ms

El algoritmo ha sido ejecutado con un tiempo varios órdenes de magnitud más bajos que con Python.

Ventajas e inconvenientes#

La ventaja más obvia es que el tiempo de ejecución ha sido sensiblemente más breve. El inconveniente principal es que hemos tenido que aprender a comunicarnos con la librería «ctypes» y su uso no resulta del todo obvio e intuitivo.

CFFI#

CFFI (C Foreign Function Interface) es una librería que nos permite también invocar y ejecutar código compilado en C desde Python. Los tiempos de ejecuación serán similares a los alcanzados con «ctypes». Para hacer uso de CFFI también tenemos que aprender sus mecanismos, en ese sentido no resulta más amable que «ctypes». Quizá la ventaja que podemos encontrar es que la manera en la que comunicamos el tipado de los objetos permite que nuestro código en Python sea más claro. De esta manera será más facil escalar y modular nuestras librerías en C.

PyBind11#

Si lo que queremos es programar nuestras rutinas externas en C++, PyBind11 puede ser la herramienta adecuada como liga entre tu código en C++ y en Python.

PyBind11 toma un abordaje diferente a los anteriores. Sus funciones pueden ser invocadas directamente. Puede resultar más cómodo.