Extendiendo Python 3 en Go

La extensión de Python ha sido una característica central de la plataforma durante décadas, el tiempo de ejecución de Python proporciona una "C API", que es un conjunto de encabezados y tipos de núcleo para escribir extensiones en C y compilarlas en módulos de Python.

Pero, ¿ realmente tiene que escribir extensiones a Python en C? ¿Por qué no podemos usar algo un poco más moderno, como Go?

Investigación de antecedentes: ¿no se ha hecho esto antes?

Encontré varios artículos antiguos escritos en 2015, ya sea para Python 2 o para Go 1.5. Go ha cambiado mucho desde entonces, por lo que encontrar algo actualizado y bien mantenido fue difícil.

Este artículo fue para Go 1.5 y usa una combinación de macros C y Go. Además, una gran limitación de cgo lugar de las funciones nativas de Go es que no admite variadics (es decir, *args ), que será un gran problema en la mayoría de los módulos de Python.

gopy , una herramienta para crear automáticamente módulos de paquetes Go. Parecía de una mirada que iba a causar problemas, ya que requiere una determinada bandera en tiempo de compilación que no se recomienda en la producción. Además, no es compatible con Python 3 .

go-python parecía un buen candidato, pero fue escrito para las vinculaciones de Python 2.

py parecía muy incompleto, solo cubría parte de la API básica de Python.

Otros ejemplos que encontré utilizan el paquete cffi , por lo que el consumo del código no se siente como un módulo, es solo una forma de llamar a los métodos compilados. Tampoco tiene enlaces nativos.

Crear bibliotecas compartidas en Go

En Go SDK, hay un conjunto de herramientas llamado cgo , que es una forma de importar encabezados C y acceder a los tipos y métodos descritos en las bibliotecas externas.

También le permite crear bibliotecas compartidas desde Go y crear encabezados C automáticamente desde sus métodos en Go lang

Toma este paquete Go realmente simple

Para entonces, ejecuta go build -buildmode=c-shared -o sum.so sum.go

Verá 3 archivos, sum.go sum.h and sum.so El archivo sum.so es el binario compilado y sum.h es el encabezado para describir qué métodos y tipos se definen dentro del binario.

Para extender Python escribiendo módulos en C, Python.h debe comenzar por importar Python.h y describir un conjunto de código de placa de caldera en C. Debido a que estamos en Go, esto se vuelve más complicado y, como han demostrado los autores anteriores, requiere macros C en Vaya, o un montón de código Go para ajustar los métodos.

O lo hace?

PyBindGen

Mucho del trabajo en el artículo original de Filippo era sobre las ataduras. Los enlaces son como usar una API REST HTTP, necesitas saber los nombres de los métodos, los parámetros esperados y las respuestas.

PyBindGen , una herramienta que ha existido durante siglos, puede crear enlaces de módulos de Python para 2 o 3, basados ??en un archivo de encabezado C o C ++. Puede incluir el código fuente en su paquete e incluso compilar e incrustar en las herramientas de configuración.

Como se muestra anteriormente, ya hemos creado ese archivo de encabezado desde el compilador Go.

Al escribir un script build.py simple, al importar PyBindGen podemos pedirle que cargue el sum.h Añádalo como una importación y describa un método en el módulo llamado Sum, que toma 2 enteros, ayb y devuelve un valor entero.

Llamar a build.py escribirá un módulo de Python en C en la pantalla. Una vez que se compila este módulo, importará el binario compilado y lo ofrecerá como un módulo nativo de Python. sum.c eso a sum.c para crear el código fuente que envuelve nuestro binario en un módulo de Python. Asegúrate de que pip install pybindgen primero.

python build.py > sum.c

Compilando el módulo

Gcc necesita más banderas que las puertas de las Naciones Unidas, así que comencemos a recopilarlas. Las rutas y opciones para nuestro tiempo de ejecución de Python, en mi caso, Python 3.6 se puede recuperar ejecutando python3.6-config --cflags y python3.6-config --ldflags

Luego llame al compilador de GCC para compilar el código C, generado por PyBindGen, que importa los métodos definidos en sum.h, que a su vez están en el binario sum.so.

gcc sum.c -dynamiclib sum.so -o testsum.so {python-flags}

Importación y prueba del módulo

Nuestro módulo tiene un único método, que suma 2 números juntos y devuelve un número. Agradable y simple.

(env) bash-3.2$ python
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type “help”, “copyright”, “credits” or “license” for more information.
>>> import testsum
>>> testsum.Sum(1,2)
3

¡Funciona! Además, la help(testsum.Sum) realmente le dice cuáles son los parámetros y qué tipos son.

Algo un poco más complejo

Todos los tutoriales en línea muestran una suma básica o función aritmética, que no es realmente tan útil. Si lees la documentación de C-API , verás que tienes acceso a los tipos comunes de Python.

Una gran limitación de cgo es que no se pueden exportar tipos complejos como struct de una función, ¡pero a quién le importa! Tenemos el impresionante tipo PyObject de Python, que puede hacer prácticamente cualquier cosa

Esta vez vamos a escribir una función que devuelve un diccionario de Python. El tipo de devolución seguirá siendo el puntero PyObject, que es el que abarca todo. Esta vez, necesitamos importar los tipos para que el compilador Go sepa cuáles son, esto es de pkg-config (que mencionamos anteriormente en Gcc). Además, como PyUnicode_FromString espera un char * , usamos una función para convertir desde una cadena Go.

Crearemos una clave y un valor del tipo str Unicode (este es Python 3 después de todo).

Probémoslo, tenemos que actualizar el archivo build.py con el nuevo método y reconstruir la biblioteca

Hemos especificado que el llamado no posee el valor de retorno (es decir, la memoria), pero hay más información disponible en la documentación sobre cómo desea manejar la recolección de basura.

(env) bash-3.2$ python
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type “help”, “copyright”, “credits” or “license” for more information.
>>> import testsum
>>> testsum.NewDictionary()
{'key': 'value'}

A continuación, ¿por qué no ver qué API y paquetes están disponibles en Go, puede comenzar la migración a Python?

¿Qué podrías lograr con Goroutines? ¿Qué pasa con la API asíncrona que está en Python 3.6? Creo que podría ser posible

¿Qué hay del rendimiento?

Puse esta función slap-dash para multiplicar 2 matrices

Curiosamente, es más lento en Go / CGo que en CPython nativo.

Posiblemente haya cometido algún tipo de error evidente aquí. También me estoy familiarizando con la API C, por lo que este código necesita verificaciones de tipos y verificaciones NULL-ref, pero la idea está ahí.

¿Todavía bloqueado en Python 2?

Vea mi nuevo curso sobre Pluralsight para pasar de Python 2 a 3.