Envolviendo una biblioteca C en Python: C, Cython o ctypes?

284

Quiero llamar a una biblioteca C desde una aplicación Python. No quiero ajustar toda la API, solo las funciones y los tipos de datos que son relevantes para mi caso. Tal como lo veo, tengo tres opciones:

  1. Cree un módulo de extensión real en C. Probablemente exagere, y también me gustaría evitar la sobrecarga de aprender a escribir la extensión.
  2. Use Cython para exponer las partes relevantes de la biblioteca C a Python.
  3. Haga todo en Python, utilizando ctypespara comunicarse con la biblioteca externa.

No estoy seguro de si 2) o 3) es la mejor opción. La ventaja de 3) es que ctypeses parte de la biblioteca estándar, y el código resultante sería Python puro, aunque no estoy seguro de cuán grande es realmente esa ventaja.

¿Hay más ventajas / desventajas con cualquiera de las dos opciones? ¿Qué enfoque me recomiendan?


Editar: Gracias por todas sus respuestas, proporcionan un buen recurso para cualquiera que quiera hacer algo similar. La decisión, por supuesto, aún debe tomarse para el caso único: no hay una respuesta de "Esto es lo correcto". Para mi propio caso, probablemente usaré ctypes, pero también estoy ansioso por probar Cython en algún otro proyecto.

Al no existir una única respuesta verdadera, aceptar una es algo arbitrario; Elegí la respuesta de FogleBird, ya que proporciona una buena idea de los tipos y actualmente es la respuesta más votada. Sin embargo, sugiero leer todas las respuestas para obtener una buena visión general.

Gracias de nuevo.

balpha
fuente
3
Hasta cierto punto, la aplicación específica involucrada (lo que hace la biblioteca) puede afectar la elección del enfoque. Hemos utilizado ctypes con bastante éxito para hablar con archivos DLL suministrados por el proveedor para varios elementos de protección (por ejemplo, osciloscopios), pero no necesariamente elegiría ctypes primero para hablar con una biblioteca de procesamiento numérico, debido a la sobrecarga adicional frente a Cython o SWIG.
Peter Hansen
1
Ahora tienes lo que estabas buscando. Cuatro respuestas diferentes (alguien también encontró SWIG). Eso significa que ahora tienes 4 opciones en lugar de 3.
Luka Rahne
@ralu Eso es lo que yo también pensé :-) Pero en serio, no esperaba (o quería) una tabla pro / con o una sola respuesta diciendo "Esto es lo que debes hacer". Cualquier pregunta sobre la toma de decisiones se responde mejor con "fanáticos" de cada opción posible dando sus razones. La votación comunitaria hace su parte, al igual que mi propio trabajo (mirar los argumentos, aplicarlos a mi caso, leer las fuentes proporcionadas, etc.). Larga historia corta: hay algunas buenas respuestas aquí.
balpha
Entonces, ¿con qué enfoque vas a ir? :)
FogleBird
1
Hasta donde sé (corríjame si me equivoco), Cython es una bifurcación de Pyrex con más desarrollo, lo que hace que Pyrex sea bastante obsoleto.
balpha

Respuestas:

115

ctypes es su mejor apuesta para hacerlo rápidamente, ¡y es un placer trabajar con él, ya que todavía está escribiendo Python!

Recientemente envolví un controlador FTDI para comunicarme con un chip USB usando ctypes y fue genial. Lo hice todo y trabajé en menos de un día de trabajo. (Solo implementé las funciones que necesitábamos, unas 15 funciones).

Anteriormente estábamos usando un módulo de terceros, PyUSB , con el mismo propósito. PyUSB es un módulo de extensión C / Python real. Pero PyUSB no estaba lanzando el GIL al bloquear lecturas / escrituras, lo que nos estaba causando problemas. Entonces escribí nuestro propio módulo usando ctypes, que libera el GIL cuando llama a las funciones nativas.

Una cosa a tener en cuenta es que los ctypes no sabrán acerca de las #defineconstantes y las cosas en la biblioteca que está utilizando, solo las funciones, por lo que tendrá que redefinir esas constantes en su propio código.

Aquí hay un ejemplo de cómo se veía el código (muchos recortados, solo tratando de mostrarte la esencia del mismo):

from ctypes import *

d2xx = WinDLL('ftd2xx')

OK = 0
INVALID_HANDLE = 1
DEVICE_NOT_FOUND = 2
DEVICE_NOT_OPENED = 3

...

def openEx(serial):
    serial = create_string_buffer(serial)
    handle = c_int()
    if d2xx.FT_OpenEx(serial, OPEN_BY_SERIAL_NUMBER, byref(handle)) == OK:
        return Handle(handle.value)
    raise D2XXException

class Handle(object):
    def __init__(self, handle):
        self.handle = handle
    ...
    def read(self, bytes):
        buffer = create_string_buffer(bytes)
        count = c_int()
        if d2xx.FT_Read(self.handle, buffer, bytes, byref(count)) == OK:
            return buffer.raw[:count.value]
        raise D2XXException
    def write(self, data):
        buffer = create_string_buffer(data)
        count = c_int()
        bytes = len(data)
        if d2xx.FT_Write(self.handle, buffer, bytes, byref(count)) == OK:
            return count.value
        raise D2XXException

Alguien hizo algunos puntos de referencia sobre las diversas opciones.

Podría dudar más si tuviera que ajustar una biblioteca C ++ con muchas clases / plantillas / etc. Pero ctypes funciona bien con estructuras e incluso puede devolver la llamada a Python.

FogleBird
fuente
55
Uniéndose a los elogios para ctypes, pero observe un problema (no documentado): ctypes no admite bifurcación. Si se bifurca de un proceso que usa ctypes, y los procesos padre e hijo continúan usando ctypes, tropezará con un error desagradable que tiene que ver con los ctypes que usan memoria compartida.
Oren Shemesh
1
@OrenShemesh ¿Hay alguna lectura adicional sobre este tema que me pueda señalar? Creo que puedo estar seguro con un proyecto en el que estoy trabajando actualmente, ya que creo que solo el proceso principal usa ctypes(for pyinotify), pero me gustaría entender el problema más a fondo.
zigg
Este pasaje me ayuda mucho One thing to note is that ctypes won't know about #define constants and stuff in the library you're using, only the functions, so you'll have to redefine those constants in your own code.Entonces, tengo que definir constantes que están allí en winioctl.h...
swdev
¿Qué hay de rendimiento? ctypeses mucho más lento que la extensión c ya que el cuello de botella es la interfaz de Python a C
TomSawyer
154

Advertencia: la opinión de un desarrollador principal de Cython por delante.

Casi siempre recomiendo Cython sobre ctypes. La razón es que tiene una ruta de actualización mucho más suave. Si usa ctypes, muchas cosas serán simples al principio, y ciertamente es genial escribir su código FFI en Python simple, sin compilación, dependencias de compilación y todo eso. Sin embargo, en algún momento, seguramente encontrará que tiene que llamar mucho a su biblioteca C, ya sea en un bucle o en una serie más larga de llamadas interdependientes, y le gustaría acelerar eso. Ese es el punto donde notarás que no puedes hacer eso con ctypes. O bien, cuando necesite funciones de devolución de llamada y descubra que su código de devolución de llamada de Python se convierte en un cuello de botella, le gustaría acelerarlo o bajarlo también a C. Nuevamente, no puedes hacer eso con ctypes.

Con Cython, OTOH, eres completamente libre de hacer que el código de envoltura y llamada sea tan delgado o grueso como quieras. Puede comenzar con llamadas simples en su código C a partir del código Python normal, y Cython las traducirá en llamadas C nativas, sin ninguna sobrecarga adicional de llamadas y con una sobrecarga de conversión extremadamente baja para los parámetros de Python. Cuando note que necesita aún más rendimiento en algún momento en el que realiza demasiadas llamadas costosas a su biblioteca de C, puede comenzar a anotar su código Python circundante con tipos estáticos y dejar que Cython lo optimice directamente en C para usted. O bien, puede comenzar a reescribir partes de su código C en Cython para evitar llamadas y especializarse y ajustar sus bucles algorítmicamente. Y si necesita una devolución de llamada rápida, simplemente escriba una función con la firma apropiada y páselo directamente al registro de devolución de llamada de C. Una vez más, no hay gastos generales, y le brinda un rendimiento de llamadas C simple. Y en el caso mucho menos probable de que realmente no pueda obtener su código lo suficientemente rápido en Cython, aún puede considerar reescribir las partes realmente críticas en C (o C ++ o Fortran) y llamarlo desde su código de Cython de forma natural y nativa. Pero entonces, esto realmente se convierte en el último recurso en lugar de la única opción.

Entonces, ctypes es bueno para hacer cosas simples y hacer que algo funcione rápidamente. Sin embargo, tan pronto como las cosas comiencen a crecer, lo más probable es que llegues al punto en que notes que es mejor que uses Cython desde el principio.

Stefan Behnel
fuente
44
+1 esos son buenos puntos, ¡muchas gracias! Aunque me pregunto si mover solo las partes del cuello de botella a Cython es realmente una sobrecarga. Pero estoy de acuerdo, si espera algún tipo de problema de rendimiento, también podría utilizar Cython desde el principio.
balpha
¿Esto todavía es válido para los programadores con experiencia en C y Python? En ese caso, se puede argumentar que Python / ctypes es la mejor opción, ya que la vectorización de los bucles C (SIMD) a veces es más sencilla. Pero, aparte de eso, no puedo pensar en ningún inconveniente de Cython.
Alex van Houten
¡Gracias por la respuesta! Una cosa con la que tuve problemas con respecto a Cython es hacer que el proceso de compilación sea correcto (pero eso también tiene que ver conmigo nunca antes había escrito un módulo de Python), ¿debería compilarlo antes o incluir los archivos fuente de Cython en sdist y preguntas similares? Escribí una publicación de blog al respecto en caso de que alguien tenga problemas / dudas similares: martinsosic.com/development/2016/02/08/…
Martinsos
¡Gracias por la respuesta! Un inconveniente cuando uso Cython es que la sobrecarga del operador no está completamente implementada (por ejemplo __radd__). Esto es especialmente molesto cuando planifica que su clase interactúe con tipos incorporados (por ejemplo, inty float). Además, los métodos mágicos en cython son un poco defectuosos en general.
Monolith
100

Cython es una herramienta bastante buena en sí misma, vale la pena aprender, y sorprendentemente se acerca a la sintaxis de Python. Si hace alguna computación científica con Numpy, entonces Cython es el camino a seguir porque se integra con Numpy para operaciones de matriz rápidas.

Cython es un superconjunto del lenguaje Python. Puede arrojar cualquier archivo Python válido y escupirá un programa C válido. En este caso, Cython solo asignará las llamadas de Python a la API de CPython subyacente. Esto da como resultado una aceleración del 50% porque su código ya no se interpreta.

Para obtener algunas optimizaciones, debe comenzar a contarle a Cython datos adicionales sobre su código, como las declaraciones de tipo. Si le dice lo suficiente, puede reducir el código a puro C. Es decir, un bucle for en Python se convierte en un bucle for en C. Aquí verá ganancias de velocidad masivas. También puede vincular a programas externos de C aquí.

Usar el código de Cython también es increíblemente fácil. Pensé que el manual hace que suene difícil. Literalmente solo haces:

$ cython mymodule.pyx
$ gcc [some arguments here] mymodule.c -o mymodule.so

y luego puedes import mymoduleen tu código de Python y olvidar por completo que se compila en C.

En cualquier caso, debido a que Cython es tan fácil de configurar y comenzar a usar, sugiero probarlo para ver si se adapta a sus necesidades. No será un desperdicio si resulta que no es la herramienta que estás buscando.

carl
fuente
1
No hay problema. Lo bueno de Cython es que solo puedes aprender lo que necesitas. Si solo desea una mejora modesta, todo lo que tiene que hacer es compilar sus archivos de Python y listo.
carl
18
"Puede arrojar cualquier archivo Python válido y generará un programa C válido". <- No del todo, hay algunas limitaciones: docs.cython.org/src/userguide/limitations.html Probablemente no sea un problema para la mayoría de los casos de uso, pero solo quería estar completo.
Randy Syring
77
Los problemas son cada vez menos con cada versión, hasta el punto que esa página ahora dice "la mayoría de los problemas se han resuelto en 0.15".
Henry Gomersall el
3
Para agregar, hay una manera MÁS fácil de importar el código de cython: escriba su código de cython como un mymod.pyxmódulo y luego hágalo import pyximport; pyximport.install(); import mymody la compilación ocurre detrás de escena.
Kaushik Ghose
3
@kaushik Aún más simple es pypi.python.org/pypi/runcython . Solo úsalo runcython mymodule.pyx. Y a diferencia de pyximport, puede usarlo para tareas de vinculación más exigentes. La única advertencia es que soy yo quien escribió las 20 líneas de bash y podría estar sesgado.
RussellStewart
42

Para llamar a una biblioteca de C desde una aplicación de Python, también existe cffi, que es una nueva alternativa para ctypes . Trae una nueva apariencia para FFI:

  • maneja el problema de una manera fascinante y limpia (a diferencia de los tipos )
  • no requiere escribir código que no sea Python (como en SWIG, Cython , ...)
Robert Zaremba
fuente
definitivamente el camino a seguir para envolver , como OP quería. cython suena genial para escribirlos hot loops, pero para las interfaces, cffi simplemente es una actualización directa de ctypes.
ovejas voladoras
21

Voy a tirar otro por ahí: SWIG

Es fácil de aprender, hace muchas cosas bien y admite muchos más idiomas, por lo que el tiempo dedicado a aprenderlo puede ser bastante útil.

Si usa SWIG, está creando un nuevo módulo de extensión de Python, pero SWIG está haciendo la mayor parte del trabajo pesado por usted.

Chris Arguin
fuente
18

Personalmente, escribiría un módulo de extensión en C. No se deje intimidar por las extensiones de Python C: no son difíciles de escribir. La documentación es muy clara y útil. Cuando escribí por primera vez una extensión C en Python, creo que me llevó alrededor de una hora descubrir cómo escribir una, no mucho tiempo.

mipadi
fuente
Envolviendo una biblioteca C. De hecho, puede encontrar el código aquí: github.com/mdippery/lehmer
mipadi
1
@forivall: El código no era realmente tan útil, y existen mejores generadores de números aleatorios. Solo tengo una copia de seguridad en mi computadora.
mipadi
2
Convenido. La C-API de Python no es tan aterradora como parece (suponiendo que conozca C). Sin embargo, a diferencia de python y su reserva de bibliotecas, recursos y desarrolladores, al escribir extensiones en C básicamente estás solo. Probablemente sea su único inconveniente (aparte de los que normalmente vienen con la escritura en C).
Noob Saibot
1
@mipadi: así, pero difieren entre Python 2.x y 3.x, por lo que es más conveniente utilizar Cython para escribir su extensión, tienen Cython averiguar todos los detalles y luego compilar el código C generado para Python 2.x o 3.x según sea necesario.
0xC0000022L
2
@mipadi parece que el enlace github está muerto y no parece estar disponible en archive.org, ¿tiene una copia de seguridad?
Jrh
11

ctypes es genial cuando ya tienes un blob de bibliotecas compilado con el que lidiar (como las bibliotecas del sistema operativo). Sin embargo, la sobrecarga de llamadas es severa, por lo que si va a hacer muchas llamadas a la biblioteca y va a escribir el código C de todos modos (o al menos compilarlo), diría que vaya a Cython . No es mucho más trabajo, y será mucho más rápido y más pitónico usar el archivo pyd resultante.

Personalmente, tiendo a usar cython para acelerar rápidamente el código de python (las comparaciones de bucles y enteros son dos áreas en las que cython brilla particularmente), y cuando haya algún código / envoltura más involucrado de otras bibliotecas involucradas, recurriré a Boost.Python . Boost.Python puede ser complicado de configurar, pero una vez que lo tienes funcionando, hace que envolver el código C / C ++ sea sencillo.

Cython también es excelente para envolver numpy (que aprendí de los procedimientos de SciPy 2009 ), pero no he usado numpy, por lo que no puedo comentar sobre eso.

Ryan Ginstrom
fuente
11

Si ya tiene una biblioteca con una API definida, creo que ctypeses la mejor opción, ya que solo tiene que hacer una pequeña inicialización y luego llamar más o menos a la biblioteca como está acostumbrado.

Creo que Cython o la creación de un módulo de extensión en C (que no es muy difícil) son más útiles cuando necesita un nuevo código, por ejemplo, llamar a esa biblioteca y realizar algunas tareas complejas y que requieren mucho tiempo, y luego pasar el resultado a Python.

Otro enfoque, para programas simples, es hacer directamente un proceso diferente (compilado externamente), enviando el resultado a la salida estándar y llamarlo con un módulo de subproceso. A veces es el enfoque más fácil.

Por ejemplo, si crea un programa de consola C que funciona más o menos de esa manera

$miCcode 10
Result: 12345678

Podrías llamarlo desde Python

>>> import subprocess
>>> p = subprocess.Popen(['miCcode', '10'], shell=True, stdout=subprocess.PIPE)
>>> std_out, std_err = p.communicate()
>>> print std_out
Result: 12345678

Con un pequeño formateo de cadenas, puede tomar el resultado de la forma que desee. También puede capturar la salida de error estándar, por lo que es bastante flexible.

Khelben
fuente
Si bien no hay nada incorrecto con esta respuesta, las personas deben ser cautelosas si el código debe ser abierto para que otros accedan, ya que llamar a un subproceso shell=Truepodría resultar fácilmente en algún tipo de explotación cuando un usuario realmente obtiene un shell. Está bien cuando el desarrollador es el único usuario, pero en el mundo hay un montón de pinchazos molestos esperando algo como esto.
Ben
7

Hay un problema que me hizo usar ctypes y no cython y que no se menciona en otras respuestas.

Usando ctypes, el resultado no depende en absoluto del compilador que esté usando. Puede escribir una biblioteca utilizando más o menos cualquier idioma que pueda compilarse en una biblioteca compartida nativa. No importa mucho, qué sistema, qué idioma y qué compilador. Cython, sin embargo, está limitado por la infraestructura. Por ejemplo, si desea usar el compilador de intel en Windows, es mucho más complicado hacer que Cython funcione: debe "explicar" el compilador a cython, recompilar algo con este compilador exacto, etc. Lo que limita significativamente la portabilidad.

Misha
fuente
4

Si está apuntando a Windows y elige envolver algunas bibliotecas C ++ propietarias, pronto descubrirá que las diferentes versiones de msvcrt***.dll(Visual C ++ Runtime) son ligeramente incompatibles.

Esto significa que es posible que no pueda usarlo Cythonya que el resultado wrapper.pydestá vinculado a msvcr90.dll (Python 2.7) o msvcr100.dll (Python 3.x) . Si la biblioteca que está ajustando está vinculada a una versión diferente del tiempo de ejecución, entonces no tiene suerte.

Luego, para que las cosas funcionen, deberá crear envoltorios de C para bibliotecas de C ++, vincular ese dll de envoltorio con la misma versión msvcrt***.dllque su biblioteca de C ++. Y luego úselo ctypespara cargar su dll de envoltura enrollada a mano dinámicamente en el tiempo de ejecución.

Así que hay muchos pequeños detalles, que se describen con gran detalle en el siguiente artículo:

"Hermosas bibliotecas nativas (en Python) ": http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/

iljau
fuente
Ese artículo no tiene nada que ver con los problemas que presenta con la compatibilidad de los compiladores de Microsoft. Hacer que las extensiones de Cython funcionen en Windows realmente no es muy difícil. He podido usar MinGW para casi todo. Sin embargo, una buena distribución de Python ayuda.
IanH
2
+1 por mencionar un posible problema en Windows (que actualmente estoy teniendo también ...). @IanH se trata menos de Windows en general, pero es un desastre si estás atascado con una lib de terceros que no coincide con tu distribución de Python.
sebastian
2

Sé que esta es una vieja pregunta, pero esto aparece en Google cuando buscas cosas como ctypes vs cython, y la mayoría de las respuestas aquí están escritas por aquellos que ya son competentes cythono cque pueden no reflejar el tiempo real que necesitas invertir para aprender esos para implementar su solución Soy un principiante completo en ambos. Nunca he tocado cythonantes, y tengo muy poca experiencia c/c++.

Durante los últimos dos días, estaba buscando una manera de delegar una parte importante de mi código en algo más bajo que Python. Implementé mi código tanto en ctypescomo Cython, que consistía básicamente en dos funciones simples.

Tenía una enorme lista de cadenas que necesitaba ser procesada. Aviso listy string. Ambos tipos no se corresponden perfectamente con los tipos c, porque las cadenas de Python son por defecto unicode y las ccadenas no lo son. Las listas en python simplemente NO son matrices de c.

Aquí está mi veredicto. Uso cython. Se integra con mayor fluidez a Python y es más fácil trabajar con él en general. Cuando algo sale mal ctypessolo te arroja por defecto, al menos cythonte dará advertencias de compilación con un seguimiento de pila siempre que sea posible, y puedes devolver un objeto python válido fácilmente cython.

Aquí hay una cuenta detallada sobre cuánto tiempo necesité invertir en ambos para implementar la misma función. Por cierto, hice muy poca programación en C / C ++:

  • Tipos:

    • Aproximadamente 2 horas para investigar cómo transformar mi lista de cadenas Unicode en un tipo compatible con CA.
    • Aproximadamente una hora sobre cómo devolver una cadena correctamente desde la función ac. Aquí en realidad proporcioné mi propia solución para SO una vez que escribí las funciones.
    • Aproximadamente media hora para escribir el código en c, compilarlo en una biblioteca dinámica.
    • 10 minutos para escribir un código de prueba en python para verificar si el ccódigo funciona.
    • Aproximadamente una hora de hacer algunas pruebas y reorganizar el ccódigo.
    • Luego conecté el ccódigo a la base de código real y vi que ctypesno funciona bien con el multiprocessingmódulo, ya que su controlador no es seleccionable por defecto.
    • Aproximadamente 20 minutos reorganicé mi código para no usar el multiprocessingmódulo y volví a intentarlo.
    • Luego, la segunda función en mi ccódigo generó segfaults en mi base de código aunque pasó mi código de prueba. Bueno, probablemente sea mi culpa por no comprobar bien los casos extremos, estaba buscando una solución rápida.
    • Durante unos 40 minutos intenté determinar las posibles causas de estos fallos.
    • Dividí mis funciones en dos bibliotecas e intenté nuevamente. Todavía tenía segfaults para mi segunda función.
    • Decidí dejar de lado la segunda función y usar solo la primera función de ccódigo y en la segunda o tercera iteración del bucle de Python que lo usa, tuve la UnicodeErrorposibilidad de no decodificar un byte en alguna posición, aunque codifiqué y decodifiqué todo explícitamente

En este punto, decidí buscar una alternativa y decidí investigar cython:

  • Cython
    • 10 min de lectura cython hello world .
    • 15 min de comprobar SO sobre cómo usar cython en setuptoolslugar de distutils.
    • 10 minutos de lectura sobre tipos de cython y tipos de python. Aprendí que puedo usar la mayoría de los tipos de python incorporados para la escritura estática.
    • 15 min de volver a anotar mi código de Python con tipos de cython.
    • 10 minutos de modificar mi setup.pypara usar el módulo compilado en mi base de código.
    • Conectado al módulo directamente a la multiprocessingversión de codebase. Funciona.

Para el registro, por supuesto, no medí los tiempos exactos de mi inversión. Puede muy bien ser el caso de que mi percepción del tiempo fuera un poco atenta debido al esfuerzo mental requerido mientras lidiaba con los tipos. Pero debe transmitir la sensación de tratar cythonyctypes

Kaan E.
fuente