Mejora de Python con extensiones de C personalizadas

    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 鈥嬧媋 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 鈥嬧媋 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.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *