Mejora de Python con extensiones de C personalizadas

Este artículo destacará las características de la API de C de CPython, que se utiliza para crear extensiones de C para Python. Voy a repasar el flujo de trabajo general...

Introducción

Este artículo destacará las características de la API de C de CPython, que se utiliza para crear extensiones de C para Python. 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 envoltorio de Python.

Quizás se esté preguntando... Python es un fantástico lenguaje 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 parte lenta particular del código de Python y (ii) se ve 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 lo que aprendí contigo.

Resumen de los pasos clave

  1. Obtener o escribir código C
  2. Escribir la función contenedora de la API de Python C
  3. Definir tabla de función(es)
  4. Definir módulo
  5. Escribir la función de inicialización
  6. Empaquete y cree la extensión

Obtención o escritura de 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 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 recorriendo cada carácter, convirtiéndolo en un int, calculando el factorial de ese int a través de factorial(long n) y agregándolo a la suma acumulativa. Finalmente devuelve la suma al código del cliente llamándolo.

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

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

Escritura de funciones contenedoras de la API C de Python

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 de Python C que usaré está 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 interfaz para las funciones que estar envolviendo También debo agregar que todos los archivos con los que estamos trabajando deben estar en el mismo directorio.

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

Ahora comenzaré a trabajar en la definición del envoltorio de la primera función de 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 sí mismo y el segundo un puntero a los argumentos pasados ​​a la función a través del código Python que llama.

1
2
3
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 la función cfactorial_sum(...) para devolver la suma factorial. Haré esto usando PyArg_ParseTuple(...).

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

Si ocurre un error en PyArg_ParseTuple(...) generará la excepción de error de tipo apropiada y el valor devuelto será cero, lo 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 llama que ocurrió una excepción.

1
2
3
4
5
6
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 funciona la función PyArg_ParseTuple(...). He construido un modelo mental alrededor de la función de modo que lo veo como tomando el número variable de argumentos posicionales pasados ​​a la función cliente de Python y capturados por el parámetro PyObject *args. Luego pienso en los argumentos capturados por el parámetro *args como desempaquetados en las variables definidas por 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


c char Cadena Python de longitud 1 convertida a C char s char array Cadena de Python convertida a C char array d double Python float convertido a C double f float Python float convertido a C float i int Python int convertido a C int l long Python int convertido a C long o PyObject * objeto de Python convertido a C PyObject

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

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

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

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

1
2
3
4
5
6
7
static PyObject *DemoLib_cFactorialSum(PyObject *self, PyObject *args) {
    // arg parsing omitted

    // factorial function call omitted

    return Py_BuildValue("i", fact_sum);
}

Estos son algunos ejemplos de algunos de los otros formatos y tipos de valores devueltos:

Código contenedor devuelto a Python


Py_BuildValue("s", "A") "A" 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("") Ninguno

¿¡Guay, verdad!?

Ahora pasemos a implementar el envoltorio para la otra función de C ifactorial_sum(...). Este contenedor incluirá algunas otras peculiaridades para trabajar.

1
2
3
4
5
6
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 la del ú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 a la función de Python se le pasará una lista que no tiene un tipo de C reconocible, necesito utilizar más herramientas de la API de Python C. El especificador de formato "O" en PyArg_ParseTuple indica que se espera un PyObject, que se asigna a la variable genérica PyObject *lst.

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 la función PyObject_Length. Si a esta función se le asigna un tipo PyObject que no implementa la interfaz de secuencia, se devuelve NULL.

1
2
3
4
    int n = PyObject_Length(lst);
    if (n < 0) {
        return NULL;
    }

Ahora que sé el tamaño de la lista, puedo convertir sus elementos en una matriz C de ints y alimentar eso en mi función ifactorial_sum C que se definió previamente. Para hacer esto, uso un ciclo 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 largo en el tipo de datos largo común de C y relleno la matriz C de largos que he llamado nums.

1
2
3
4
5
6
  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 función ifactorial_sum(...) pasándole nums y n, que devuelve la suma factorial de la matriz de largos. Nuevamente, usaré Py_BuildValue para convertir la suma nuevamente en un Python int y devolverlo al código de Python del cliente que llama.

1
2
3
4
    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 estándar de la API de Python C que dedicaré menos tiempo a explicar y remitiré al lector a los [documentos](https://docs.python.org/3.6/c-api/index. html) para más detalles.

Definir tabla de función(es)

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_VARARGS, y proporciona una cadena de documentación a nivel de función.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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 la matriz DemoLib_FunctionsTable previamente definida 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.

1
2
3
4
5
6
7
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
};

Escriba 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 donde 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.

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#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 {#empaquetado y construcción de la extensión}

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

Lo primero que tendré que hacer es instalar las herramientas de configuración:

1
$ pip install setuptools

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

1
2
3
4
├── demolib.c
├── demolib.h
├── demomodule.c
└── setup.py

Dentro de setup.py coloque el siguiente código, que importa la clase Extension y la función de configuración de setuptools. Ejemplifico la clase Extension que se usa para compilar el código C usando el compilador gcc, que está instalado de forma nativa en la mayoría de los sistemas operativos de 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 ubicará el archivo setup.py y llamará a su función setup(...):

1
$ pip install .

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

1
2
3
4
5
6
7
8
$  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 realmente apenas rasca la superficie de la API de Python C, que me pareció un tema enorme y desalentador. Espero que, si necesita ampliar Python, este tutorial junto con los documentos oficiales lo 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. ón.

Licensed under CC BY-NC-SA 4.0