Mejora de Python con extensiones de C personalizadas

M

Introducción

Este artículo destacará las características de la API C de CPython que se utiliza para crear extensiones C para Python. Voy a repasar el flujo de trabajo general para tomar una pequeña biblioteca de funciones C bastante banales, de ejemplo de juguete, y exponerlas en un contenedor de Python.

Quizás se esté preguntando … Python es un lenguaje fantástico de alto nivel capaz de casi cualquier cosa, ¿por qué querría lidiar con un código C desordenado? Y tendría que estar de acuerdo con la premisa general de ese argumento. Sin embargo, hay dos casos de uso comunes que he encontrado en los que es probable que surja esto: (i) para acelerar una pieza lenta en particular de código Python y, (ii) está obligado a incluir un programa ya escrito en C en un establezca el programa Python y no desea volver a escribir el código C en Python. Esto último me sucedió recientemente y quería compartir con ustedes lo que he aprendido.

Resumen de pasos clave

  • Obtener o escribir código C
  • Escribir la función contenedora de la API de Python C
  • Definir tabla de funciones
  • Definir módulo
  • Escribir función de inicialización
  • Empaqueta y crea la extensión

Obtener o escribir código C

Para este tutorial, trabajaré con un pequeño conjunto de funciones de C que escribí con mi conocimiento limitado de C. Todos los programadores de C que lean esto, tengan piedad de mí por el código que están a punto de ver.

// demolib.h
unsigned long cfactorial_sum(char num_chars[]);
unsigned long ifactorial_sum(long nums[], int size);
unsigned long factorial(long n);

#include <stdio.h>
#include "demolib.h"

unsigned long cfactorial_sum(char num_chars[]) {
    unsigned long fact_num;
    unsigned long sum = 0;

    for (int i = 0; num_chars[i]; i++) {
        int ith_num = num_chars[i] - '0';
        fact_num = factorial(ith_num);
        sum = sum + fact_num;
    }
    return sum;
}

unsigned long ifactorial_sum(long nums[], int size) {
    unsigned long fact_num;
    unsigned long sum = 0;
    for (int i = 0; i < size; i++) {
        fact_num = factorial(nums[i]);
        sum += fact_num;
    }
    return sum;
}

unsigned long factorial(long n) {
    if (n == 0)
        return 1;
    return (unsigned)n * factorial(n-1);
}

El primer archivo demolib.h es un archivo de encabezado C que define las firmas de funciones con las que trabajaré y el segundo archivo demolib.c muestra las implementaciones reales de esas funciones.

La primera función cfactorial_sum(char num_chars[]) recibe una cadena C de dígitos numéricos representados por una matriz de caracteres donde cada carácter es un número. La función construye una suma haciendo un bucle sobre cada carácter, convirtiéndolo en un int, calculando el factorial de ese int a través de factorial(long n) y sumarlo a la suma acumulada. Finalmente devuelve la suma al código del cliente que lo llama.

La segunda función ifactorial_sum(long nums[], int size) se comporta de manera similar a sfactorial_sum(...), pero sin la necesidad de convertir a ints.

La última función es simple factorial(long n) función implementada en un algoritmo de tipo recursivo.

Escritura de funciones de envoltura de API de Python C

Escribir la función contenedora de C a Python es la parte más complicada de todo el proceso que voy a demostrar. La API de extensión Python C que usaré se encuentra en el archivo de encabezado C Python.h, que viene incluido con la mayoría de las instalaciones de CPython. Para el propósito de este tutorial, usaré la distribución anaconda de CPython 3.6.

Lo primero es lo primero, incluiré el archivo de encabezado Python.h en la parte superior de un nuevo archivo llamado demomodule.c, y también incluiré mi archivo de encabezado personalizado demolib.h, ya que sirve como una interfaz para las funciones que haré estar envolviendo. También debo agregar que todos los archivos con los que estamos trabajando deben estar en el mismo directorio.

// demomodule.c
#include <Python.h>
#include "demolib.h"

Ahora comenzaré a trabajar en la definición del contenedor para la primera función C cfactorial_sum(...). La función debe ser estática, ya que su alcance debe limitarse solo a este archivo y debe devolver un PyObject expuesto a nuestro programa a través del archivo de encabezado Python.h. El nombre de la función contenedora será DemoLib_cFactorialSum y contendrá dos argumentos, ambos de tipo PyObject siendo el primero un puntero a self y el segundo un puntero a los argumentos pasados ​​a la función a través del código Python de llamada.

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    ...
}

A continuación, necesito analizar la cadena de dígitos que el código Python del cliente pasará a esta función y convertirla en una matriz de caracteres C para que pueda ser utilizada por el cfactorial_sum(...) función para devolver la suma factorial. Haré esto usando PyArg_ParseTuple(...).

Primero tendré que definir un puntero char C llamado char_nums que recibirá el contenido de la cadena de Python que se pasa a la función. Luego llamaré PyArg_ParseTuple(...) pasándolo el PyObject valor de args, una cadena de formato "s" que especifica que el primer (y único) parámetro de args es una cadena que debe ser coaccionada en el último argumento, el char_nums variable.

Si ocurre un error en PyArg_ParseTuple(...) generará la excepción de error de tipo apropiado y el valor de retorno será cero, que se interpreta como falso en un condicional. Si se detecta un error en mi declaración if, devuelvo un NULL, que indica al código Python que realiza la llamada que se produjo una excepción.

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    char *char_nums;
    if (!PyArg_ParseTuple(args, "s", &char_nums)) {
        return NULL:
    }
}

Me gustaría tomarme un poco de tiempo para hablar sobre cómo PyArg_ParseTuple(...) la función funciona. He construido un modelo mental alrededor de la función de tal manera que lo veo tomando el número variable de argumentos posicionales pasados ​​a la función cliente Python y capturados por el PyObject *args parámetro. Luego pienso en los argumentos capturados por el *args parámetro como descomprimido en las variables definidas en C que vienen después del especificador de cadena de formato.

La siguiente tabla muestra lo que creo que son los especificadores de formato más utilizados.

Especificador C Tipo Descripción

CcarbonizarseCadena de Python de longitud 1 convertida a C char
smatriz de caracteresCadena de Python convertida a matriz de caracteres C
redobleFlotador de Python convertido en un doble de C
FflotadorFlotador de Python convertido en un flotador C
yoEn tPython int convertido a C int
llargoPython int convertido a C long
oPyObject *Objeto de Python convertido a un PyObject en C

Si está pasando varios argumentos a una función que se desempaquetarán y convertirán en tipos C, simplemente use múltiples especificadores como PyArg_ParseTuple(args, "si", &charVar, &intVar).

Ok, ahora que tenemos una idea de cómo PyArg_ParseTuple(...) obras voy a seguir adelante. Lo siguiente que debe hacer es llamar al cfactorial_sum(...) función pasándole el char_nums matriz que acabamos de construir a partir de la cadena de Python que se pasó al contenedor. La devolución será un largo sin firmar.

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    // arg parsing omitted
    unsigned long fact_sum;
    fact_sum = cfactorial_sum(char_nums);
}

La última cosa que hacer en el DemoLib_cFactorialSum(...) La función contenedora es devolver la suma en una forma con la que el código Python del cliente pueda trabajar. Para hacer esto utilizo otra herramienta llamada Py_BuildValue(...) expuesto a través del tesoro de Python.h. Py_BuildValue utiliza especificadores de formato muy similares a cómo PyArg_ParseTuple(...) los usa, solo en la dirección opuesta. Py_BuildValue también permite devolver nuestras estructuras de datos de Python familiares, como tuplas y dictados. En esta función contenedora, devolveré un int a Python, que implemento de la siguiente manera:

static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    // arg parsing omitted

    // factorial function call omitted

    return Py_BuildValue("i", fact_sum);
}

A continuación, se muestran algunos ejemplos de algunos de los otros tipos y formatos de valor de retorno:

Código contenedor devuelto a Python

Py_BuildValue (“s”, “A”)“UN”
Py_BuildValue (“i”, 10)10
Py_BuildValue (“(iii)”, 1, 2, 3)(1, 2, 3)
Py_BuildValue (“{si, si}”, “a ‘, 4,” b “, 9){“a”: 4, “b”: 9}
Py_BuildValue (“”)Ninguna

¿¡Guay, verdad!?

Ahora pasemos a implementar el contenedor en la otra función de C ifactorial_sum(...). Esta envoltura incluirá algunas otras peculiaridades para resolver.

static PyObject *DemoLib_iFactorialSum(PyObject *self, PyObject *args) {
    PyObject *lst;
    if(!PyArg_ParseTuple(args, "O", &lst)) {
        return NULL;
    }
}

Como puede ver, la firma de la función es la misma que en el último ejemplo en que es estática, devuelve un PyObject, y los parámetros son dos PyObjects. Sin embargo, el análisis de argumentos es un poco diferente. Dado que la función de Python recibirá una lista que no tiene un tipo C reconocible, necesito utilizar más herramientas de la API de Python C. El especificador de formato “O” en PyArg_ParseTuple indica que un PyObject se espera, que se asigna al genérico PyObject *lst variable.

Detrás de escena, la maquinaria de la API de Python C reconoce que el argumento pasado implementa la interfaz de secuencia, lo que me permite obtener el tamaño de la lista pasada usando el PyObject_Length función. Si a esta función se le da un PyObject tipo que no implementa la interfaz de secuencia, entonces un NULL es regresado.

    int n = PyObject_Length(lst);
    if (n < 0) {
        return NULL;
    }

Ahora que conozco el tamaño de la lista, puedo convertir sus elementos en una matriz C de entradas y alimentarlo en mi ifactorial_sum Función C que se definió previamente. Para hacer esto, uso un bucle for para iterar sobre los elementos de la lista, recuperando cada elemento usando PyList_GetItem, que devuelve un PyObject implementado como una representación de Python de un largo llamado PyLongObject. Luego uso PyLong_AsLong para convertir la representación de Python de un long en el tipo de datos long C long común y completar la matriz C de longs que he nombrado nums.

  long nums[n];
  for (int i = 0; i < n; i++) {
    PyLongObject *item = PyList_GetItem(lst, i);
    long num = PyLong_AsLong(item);
    nums[i] = num;
  }

En este punto puedo llamar a mi ifactorial_sum(...) función pasándolo nums y n, que devuelve la suma factorial de la matriz de longs. De nuevo, usaré Py_BuildValue para volver a convertir la suma en un int de Python y devolverla al código Python del cliente que realiza la llamada.

    unsigned long fact_sum;
    fact_sum = ifactorial_sum(nums, n);

    return Py_BuildValue("i", fact_sum);

El resto del código que se escribirá es simplemente un código repetitivo de la API de Python C, que dedicaré menos tiempo a explicar y remitiré al lector al docs para detalles.

Definir tabla de funciones

En esta sección escribiré una matriz que asocia las dos funciones contenedoras escritas en la sección anterior al nombre que se expondrá en Python. Esta matriz también indica el tipo de argumentos que se pasan a nuestras funciones, METH_VARARGSy proporciona una cadena de documentos a nivel de función.

static PyMethodDef DemoLib_FunctionsTable[] = {
    {
        "sfactorial_sum",      // name exposed to Python
        DemoLib_cFactorialSum, // C wrapper function
        METH_VARARGS,          // received variable args (but really just 1)
        "Calculates factorial sum from digits in string of numbers" // documentation
    }, {
        "ifactorial_sum",      // name exposed to Python
        DemoLib_iFactorialSum, // C wrapper function
        METH_VARARGS,          // received variable args (but really just 1)
        "Calculates factorial sum from list of ints" // documentation
    }, {
        NULL, NULL, 0, NULL
    }
};

Definir módulo

Aquí proporcionaré una definición de módulo que asocia el definido previamente DemoLib_FunctionsTable matriz al módulo. Esta estructura también es responsable de definir el nombre del módulo que se expone en Python, así como de proporcionar una cadena de documentación a nivel de módulo.

static struct PyModuleDef DemoLib_Module = {
    PyModuleDef_HEAD_INIT,
    "demo",     // name of module exposed to Python
    "Demo Python wrapper for custom C extension library.", // module documentation
    -1,
    DemoLib_FunctionsTable
};

Escribir la función de inicialización

El último bit de código C-ish para escribir es la función de inicialización del módulo, que es el único miembro no estático del código contenedor. Esta función tiene una convención de nomenclatura muy particular de PyInit_name dónde name es el nombre del módulo. Esta función se invoca en el intérprete de Python, que crea el módulo y lo hace accesible.

PyMODINIT_FUNC PyInit_demo(void) {
    return PyModule_Create(&DemoLib_Module);
}

El código de extensión completo ahora se ve así:

#include <stdio.h>
#include <Python.h>
#include "demolib.h"

// wrapper function for cfactorial_sum
static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    char *char_nums;
    if (!PyArg_ParseTuple(args, "s", &char_nums)) {
        return NULL;
    }

    unsigned long fact_sum;
    fact_sum = cfactorial_sum(char_nums);

    return Py_BuildValue("i", fact_sum);
}

// wrapper function for ifactorial_sum
static PyObject *DemoLib_iFactorialSum(PyObject *self, PyObject *args) {
    PyObject *lst;
    if (!PyArg_ParseTuple(args, "O", &lst)) {
        return NULL;
    }

    int n = PyObject_Length(lst);
    if (n < 0) {
        return NULL;
    }

    long nums[n];
    for (int i = 0; i < n; i++) {
        PyLongObject *item = PyList_GetItem(lst, i);
        long num = PyLong_AsLong(item);
        nums[i] = num;
    }

    unsigned long fact_sum;
    fact_sum = ifactorial_sum(nums, n);

    return Py_BuildValue("i", fact_sum);
}

// module's function table
static PyMethodDef DemoLib_FunctionsTable[] = {
    {
        "sfactorial_sum", // name exposed to Python
        DemoLib_cFactorialSum, // C wrapper function
        METH_VARARGS, // received variable args (but really just 1)
        "Calculates factorial sum from digits in string of numbers" // documentation
    }, {
        "ifactorial_sum", // name exposed to Python
        DemoLib_iFactorialSum, // C wrapper function
        METH_VARARGS, // received variable args (but really just 1)
        "Calculates factorial sum from list of ints" // documentation
    }, {
        NULL, NULL, 0, NULL
    }
};

// modules definition
static struct PyModuleDef DemoLib_Module = {
    PyModuleDef_HEAD_INIT,
    "demo",     // name of module exposed to Python
    "Demo Python wrapper for custom C extension library.", // module documentation
    -1,
    DemoLib_FunctionsTable
};

PyMODINIT_FUNC PyInit_demo(void) {
    return PyModule_Create(&DemoLib_Module);
}

Empaquetado y construcción de la extensión

Ahora empaquetaré y construiré la extensión para poder usarla en Python con la ayuda del herramientas de configuración biblioteca.

Lo primero que tendré que hacer es instalar setuptools:

$ pip install setuptools

Ahora crearé un nuevo archivo llamado setup.py. A continuación se muestra una representación de cómo están organizados mis archivos:

├── demolib.c
├── demolib.h
├── demomodule.c
└── setup.py

Dentro de setup.py coloque el siguiente código, que importa el Extension class y la función de configuración de setuptools. Yo instancia el Extension clase que se usa para compilar el código C usando el compilador gcc, que se instala de forma nativa en la mayoría de los sistemas operativos estilo Unix. Los usuarios de Windows querrán instalar MinGW.

El último fragmento de código que se muestra simplemente pasa la información mínima sugerida para empaquetar el código en un paquete de Python.

from setuptools import Extension, setup

module = Extension("demo",
                  sources=[
                    'demolib.c',
                    'demomodule.c'
                  ])
setup(name="demo",
     version='1.0',
     description='Python wrapper for custom C extension',
     ext_modules=[module])

En un shell, ejecutaré el siguiente comando para compilar e instalar el paquete en mi sistema. Este código localizará el archivo setup.py y llamará a su setup(...) función:

$ pip install .

Finalmente, ahora puedo iniciar un intérprete de Python, importar mi módulo y probar mis funciones de extensión:

$  python
Python 3.6.4 |Anaconda, Inc.| (default, Dec 21 2017, 15:39:08)
>>> import demo
>>> demo.sfactorial_sum("12345")
153
>>> demo.ifactorial_sum([1,2,3,4,5])
153
>>>

Conclusión

En mis comentarios finales, me gustaría decir que este tutorial apenas rasca la superficie de la API de Python C, que me pareció un tema enorme y abrumador. Espero que, en caso de que necesite ampliar Python, este tutorial junto con los documentos oficiales le ayuden a lograr ese objetivo.

Gracias por leer y doy la bienvenida a todos y cada uno de los comentarios o críticas a continuación.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con tus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad