Python es un lenguaje fantástico para productividad, pero hay situaciones donde su rendimiento no basta: procesamiento numérico intensivo, operaciones sobre grandes volúmenes de datos o código de sistema que necesita acceso directo a hardware. La solución clásica es escribir la parte crítica en C y llamarla desde Python. Hay tres formas principales de hacerlo: ctypes, la Python C API y cffi.
ctypes: llamar a una biblioteca .so sin recompilar
ctypes es la opción más rápida de implementar: carga una biblioteca compartida ya compilada y llama a sus funciones directamente. No requiere modificar el código C ni Python.
La biblioteca C
/* operaciones.c */
#include <math.h>
int sumar(int a, int b) {
return a + b;
}
double distancia(double x1, double y1, double x2, double y2) {
double dx = x2 - x1;
double dy = y2 - y1;
return sqrt(dx*dx + dy*dy);
}
/* Compilar como biblioteca compartida */
/* gcc -shared -fPIC -o liboperaciones.so operaciones.c -lm */
Usar desde Python con ctypes
from ctypes import cdll, c_int, c_double
# Cargar la biblioteca
lib = cdll.LoadLibrary('./liboperaciones.so')
# Declarar tipos de argumentos y retorno (obligatorio para correctitud)
lib.sumar.argtypes = [c_int, c_int]
lib.sumar.restype = c_int
lib.distancia.argtypes = [c_double, c_double, c_double, c_double]
lib.distancia.restype = c_double
# Llamar
resultado = lib.sumar(10, 32)
print(resultado) # 42
d = lib.distancia(0.0, 0.0, 3.0, 4.0)
print(d) # 5.0
Declarar argtypes y restype es fundamental: sin ellos, ctypes asume que los argumentos son int de 32 bits y el retorno también, lo que provoca resultados incorrectos con doubles o punteros.
Strings y punteros con ctypes
from ctypes import c_char_p, create_string_buffer lib.procesar_texto.argtypes = [c_char_p] lib.procesar_texto.restype = c_char_p # Pasar string Python a C resultado = lib.procesar_texto(b"hola mundo") print(resultado.decode()) # Buffer mutable para que C escriba en él buf = create_string_buffer(256) lib.rellenar_buffer(buf, 256) print(buf.value.decode())
Python C API: módulos nativos completos
La C API permite crear módulos Python completos en C, con soporte para clases, excepciones y el GIL. Es la forma en que están escritos NumPy, PIL y la mayor parte de la librería estándar de CPython.
Módulo C mínimo
/* mimodulo.c */
#define PY_SSIZE_T_CLEAN
#include <Python.h>
/* Función que Python llamará como mimodulo.sumar(a, b) */
static PyObject* py_sumar(PyObject* self, PyObject* args) {
int a, b;
if (!PyArg_ParseTuple(args, "ii", &a, &b))
return NULL; /* error de parseo: PyArg_ParseTuple ya pone la excepción */
return PyLong_FromLong(a + b);
}
/* Tabla de métodos del módulo */
static PyMethodDef metodos[] = {
{"sumar", py_sumar, METH_VARARGS, "Suma dos enteros"},
{NULL, NULL, 0, NULL} /* centinela */
};
/* Definición del módulo */
static struct PyModuleDef modulo = {
PyModuleDef_HEAD_INIT,
"mimodulo", /* nombre */
NULL, /* docstring */
-1,
metodos
};
/* Función de inicialización: DEBE llamarse PyInit_NOMBRE */
PyMODINIT_FUNC PyInit_mimodulo(void) {
return PyModule_Create(&modulo);
}
Compilar e instalar
# Obtener flags de compilación
python3-config --includes --ldflags
# Compilar manualmente
gcc -shared -fPIC
$(python3-config --includes)
$(python3-config --ldflags)
-o mimodulo.so mimodulo.c
# Usar desde Python
python3 -c "import mimodulo; print(mimodulo.sumar(3, 4))"
Para proyectos reales, se usa setup.py o pyproject.toml con setuptools y la clase Extension, que gestiona los flags automáticamente.
Gestión del reference counting
La parte más delicada de la C API es el conteo de referencias manual. Cada PyObject* tiene un contador; cuando llega a 0, el GC libera el objeto:
/* Py_INCREF: incrementar contador (cuando guardamos una referencia) */ /* Py_DECREF: decrementar (cuando la liberamos) */ PyObject* lista = PyList_New(3); /* lista tiene refcount=1 */ PyObject* num = PyLong_FromLong(42); PyList_SetItem(lista, 0, num); /* SetItem roba la referencia de num: no llamar Py_DECREF(num) */ Py_DECREF(lista); /* liberar la lista al terminar */
cffi: la alternativa moderna
cffi es más ergonómica que ctypes y más sencilla que la C API completa. Define la interfaz directamente en C:
from cffi import FFI
ffi = FFI()
ffi.cdef("""
int sumar(int a, int b);
double distancia(double x1, double y1, double x2, double y2);
""")
lib = ffi.dlopen('./liboperaciones.so')
print(lib.sumar(10, 32)) # 42
print(lib.distancia(0, 0, 3, 4)) # 5.0
cffi es la interfaz que usa PyPy internamente y tiene mejor soporte para estructuras C complejas que ctypes.
Cuándo usar cada opción
- ctypes: tienes una .so ya compilada y necesitas llamarla rápido. Sin código extra en C.
- cffi: mismas condiciones que ctypes pero con API más limpia y mejor soporte de structs.
- C API: necesitas integración profunda con Python (clases, generadores, excepciones personalizadas) o máximo rendimiento con control del GIL.
Si te interesa el rendimiento en C puro, el artículo sobre gestión de memoria en C cubre cómo optimizar las reservas que luego expones a Python. Y si buscas una alternativa con interoperabilidad nativa con C sin el overhead del GIL, Zig 0.14 ofrece interoperabilidad directa con bibliotecas C sin FFI intermedia.
Imagen: Pexels / Myburgh Roux
