Quebrando uma biblioteca C em Python: C, Cython ou ctypes?

284

Eu quero chamar uma biblioteca C de um aplicativo Python. Não quero agrupar a API inteira, apenas as funções e tipos de dados que são relevantes para o meu caso. A meu ver, tenho três opções:

  1. Crie um módulo de extensão real em C. Provavelmente exagere e eu também gostaria de evitar a sobrecarga de aprender a escrever extensões.
  2. Use o Cython para expor as partes relevantes da biblioteca C ao Python.
  3. Faça tudo em Python, usando ctypespara se comunicar com a biblioteca externa.

Não tenho certeza se 2) ou 3) é a melhor escolha. A vantagem de 3) é que ctypesfaz parte da biblioteca padrão, e o código resultante seria puro Python - embora eu não tenha certeza do tamanho da vantagem.

Existem mais vantagens / desvantagens em qualquer uma das opções? Qual abordagem você recomenda?


Edit: Obrigado por todas as suas respostas, eles fornecem um bom recurso para quem quer fazer algo semelhante. A decisão, é claro, ainda deve ser tomada para o caso único - não há um tipo de resposta "Esta é a coisa certa". No meu caso, provavelmente vou usar ctypes, mas também estou ansioso para experimentar o Cython em outro projeto.

Como não existe uma única resposta verdadeira, aceitar uma é um tanto arbitrário; Eu escolhi a resposta do FogleBird, pois fornece algumas boas dicas sobre os tipos e, atualmente, também é a resposta mais votada. No entanto, sugiro ler todas as respostas para obter uma boa visão geral.

Obrigado novamente.

balpha
fonte
3
Até certo ponto, o aplicativo específico envolvido (o que a biblioteca faz) pode afetar a escolha da abordagem. Usamos o ctypes com bastante êxito para conversar com DLLs fornecidas pelo fornecedor para várias peças de hardware (por exemplo, osciloscópios), mas eu não necessariamente escolhia os ctypes primeiro para conversar com uma biblioteca de processamento numérico, devido à sobrecarga extra em comparação com Cython ou SWIG.
Peter Hansen
1
Agora você tem o que estava procurando. Quatro respostas diferentes. (Alguém também encontrou SWIG). Isso significa que agora você tem 4 opções em vez de 3.
Luka Rahne
@ralu Isso é o que eu também acho :-) Mas, falando sério, eu não esperava (ou queria) uma tabela pró / contra ou uma única resposta dizendo "Eis o que você precisa fazer". Qualquer pergunta sobre tomada de decisão é melhor respondida com "fãs" de cada opção possível, apresentando suas razões. A votação da comunidade faz sua parte, assim como meu próprio trabalho (analisando os argumentos, aplicando-os ao meu caso, lendo as fontes fornecidas, etc.). Resumindo: Há boas respostas aqui.
balpha
Então, com qual abordagem você vai seguir? :)
FogleBird 22/12/2009
1
Tanto quanto eu sei (por favor, corrija-me se estiver errado), o Cython é um fork do Pyrex com mais desenvolvimento, tornando o Pyrex praticamente obsoleto.
balpha

Respostas:

115

ctypes é a sua melhor aposta para fazê-lo rapidamente, e é um prazer trabalhar enquanto você ainda está escrevendo Python!

Recentemente, envolvi um driver FTDI para comunicação com um chip USB usando ctypes e foi ótimo. Eu fiz tudo e trabalhei em menos de um dia de trabalho. (Eu apenas implementei as funções que precisávamos, cerca de 15 funções).

Anteriormente, estávamos usando um módulo de terceiros, PyUSB , para o mesmo objetivo. PyUSB é um módulo de extensão C / Python real. Mas o PyUSB não estava liberando o GIL ao bloquear as leituras / gravações, o que estava causando problemas para nós. Então, eu escrevi nosso próprio módulo usando ctypes, que libera o GIL ao chamar as funções nativas.

Uma coisa a observar é que os ctypes não sabem sobre #defineconstantes e outras coisas na biblioteca que você está usando, apenas as funções, então você terá que redefinir essas constantes em seu próprio código.

Aqui está um exemplo de como o código acabou aparecendo (lotes cortados, apenas tentando mostrar a essência dele):

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

Alguém fez alguns benchmarks nas várias opções.

Eu poderia estar mais hesitante se tivesse que quebrar uma biblioteca C ++ com muitas classes / modelos / etc. Mas o ctypes funciona bem com estruturas e pode até retornar para o Python.

FogleBird
fonte
5
Juntando elogios a ctypes, mas observe um problema (não documentado): ctypes não suporta bifurcação. Se você sair de um processo usando ctypes, e os processos pai e filho continuarem usando ctypes, você encontrará um bug desagradável relacionado a ctypes usando memória compartilhada.
Oren Shemesh
1
@OrenShemesh Há mais alguma leitura sobre esse assunto que você possa me indicar? Acho que posso estar seguro com um projeto no qual estou trabalhando atualmente, pois acredito que apenas o processo pai use ctypes(para pyinotify), mas gostaria de entender o problema mais detalhadamente.
Zigg
Esta passagem me ajuda muito 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.Então, eu tenho que definir constantes que estão lá em winioctl.h....
swdev
e quanto ao desempenho? ctypesé muito mais lento que a extensão c, já que o gargalo é a interface do Python para o C
TomSawyer
154

Aviso: a opinião de um desenvolvedor central do Cython à frente.

Eu quase sempre recomendo o Cython sobre ctypes. O motivo é que ele possui um caminho de atualização muito mais suave. Se você usa ctypes, muitas coisas serão simples no início, e certamente é legal escrever seu código FFI em Python simples, sem compilação, compilar dependências e tudo mais. No entanto, em algum momento, você quase certamente descobrirá que precisa chamar muito a sua biblioteca C, seja em loop ou em uma série mais longa de chamadas interdependentes, e gostaria de acelerar isso. É nesse ponto que você notará que não pode fazer isso com ctypes. Ou, quando você precisar de funções de retorno de chamada e descobrir que seu código de retorno de chamada Python se torna um gargalo, você gostaria de acelerá-lo e / ou movê-lo para C também. Novamente, você não pode fazer isso com ctypes.

Com o Cython, OTOH, você é totalmente livre para tornar o código de agrupamento e chamada tão fino ou grosso quanto desejar. Você pode começar com chamadas simples para o seu código C a partir do código Python comum, e o Cython as converterá em chamadas C nativas, sem nenhuma sobrecarga adicional de chamadas e com uma sobrecarga de conversão extremamente baixa para os parâmetros do Python. Quando você notar que precisa de ainda mais desempenho em algum momento em que está fazendo muitas chamadas caras na sua biblioteca C, pode começar a anotar seu código Python ao redor com tipos estáticos e permitir que o Cython o otimize diretamente em C para você. Ou então, você pode começar a reescrever partes do seu código C no Cython, a fim de evitar chamadas e especializar e apertar seus loops algoritmicamente. E se você precisar de um retorno de chamada rápido, basta escrever uma função com a assinatura apropriada e passá-la diretamente para o registro de retorno de chamada C. Novamente, sem sobrecarga, e oferece desempenho simples de chamada em C. E no caso muito menos provável de que você realmente não consiga obter seu código com rapidez suficiente no Cython, você ainda pode reescrever as partes realmente críticas dele em C (ou C ++ ou Fortran) e chamá-lo do seu código Cython de forma natural e nativa. Mas, então, isso realmente se torna o último recurso, em vez da única opção.

Portanto, o ctypes é bom para fazer coisas simples e executar rapidamente algo. No entanto, assim que as coisas começarem a crescer, você provavelmente chegará ao ponto em que perceberá que utilizou melhor o Cython desde o início.

Stefan Behnel
fonte
4
+1 esses são bons pontos, muito obrigado! Embora eu me pergunte se mover apenas as partes de gargalo para o Cython é realmente uma sobrecarga. Mas concordo que, se você espera algum tipo de problema de desempenho, pode usar o Cython desde o início.
balpha
Isso ainda vale para programadores experientes em C e Python? Nesse caso, pode-se argumentar que Python / ctypes é a melhor escolha, já que a vetorização de loops C (SIMD) às vezes é mais direta. Mas, além disso, não consigo pensar em nenhuma desvantagem do Cython.
Alex van Houten
Obrigado pela resposta! Uma coisa com a qual tive problemas em relação ao Cython é obter o processo de compilação correto (mas isso também tem a ver comigo nunca escrever um módulo Python antes) - devo compilá-lo antes ou incluir arquivos de origem Cython em sdist e perguntas semelhantes. Eu escrevi um post sobre isso no caso de alguém ter problemas / dúvidas semelhantes: martinsosic.com/development/2016/02/08/…
Martinsos
Obrigado pela resposta! Uma desvantagem quando uso o Cython é que a sobrecarga do operador não está totalmente implementada (por exemplo __radd__). Isso é especialmente irritante quando você planeja que sua classe interaja com os tipos internos (por exemplo, inte float). Além disso, métodos mágicos em cython são um pouco problemáticos em geral.
Monolith
100

O Cython é uma ferramenta bastante interessante por si só, vale a pena aprender e é surpreendentemente próxima da sintaxe do Python. Se você faz qualquer computação científica com o Numpy, o Cython é o caminho a seguir, porque ele se integra ao Numpy para operações de matriz rápida.

Cython é um superconjunto da linguagem Python. Você pode lançar qualquer arquivo Python válido, e ele emitirá um programa C válido. Nesse caso, o Cython apenas mapeará as chamadas do Python para a API CPython subjacente. Isso resulta em talvez uma aceleração de 50% porque seu código não é mais interpretado.

Para obter algumas otimizações, você deve começar a contar fatos adicionais ao Cython sobre seu código, como declarações de tipo. Se você disser o suficiente, ele pode reduzir o código para C. puro. Ou seja, um loop for no Python se torna um loop for no C. Aqui você verá ganhos de velocidade maciços. Você também pode vincular a programas externos C aqui.

Usar o código Cython também é incrivelmente fácil. Eu pensei que o manual faz parecer difícil. Você literalmente faz:

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

e então você pode import mymodule no seu código Python e esquecer completamente que ele é compilado até C.

De qualquer forma, como o Cython é tão fácil de configurar e começar a usar, sugiro que tente se ele atende às suas necessidades. Não será um desperdício se não for a ferramenta que você está procurando.

carl
fonte
1
Sem problemas. O bom do Cython é que você pode aprender apenas o que precisa. Se você quer apenas uma melhoria modesta, tudo que você precisa fazer é compilar seus arquivos Python e pronto.
carl
18
"Você pode lançar qualquer arquivo Python válido, e ele cuspirá um programa C válido". <- Não exatamente, existem algumas limitações: docs.cython.org/src/userguide/limitations.html Provavelmente não é um problema para a maioria dos casos de uso, mas apenas deseja estar completo.
Randy Syring
7
Os problemas estão diminuindo a cada versão, a tal ponto que agora a página diz que "a maioria dos problemas foi resolvida em 0,15".
Henry Gomersall
3
Para adicionar, existe ainda uma maneira mais fácil de importar código cython: escreva seu código cython como um mymod.pyxmódulo e faça, import pyximport; pyximport.install(); import mymode a compilação acontece nos bastidores.
Kaushik Ghose
3
@kaushik Ainda mais simples é pypi.python.org/pypi/runcython . Apenas use runcython mymodule.pyx. E, diferentemente do pyximport, você pode usá-lo para tarefas de vinculação mais exigentes. A única ressalva é que fui eu quem escreveu as 20 linhas do bash e pode ser tendenciosa.
RussellStewart
42

Para chamar uma biblioteca C a partir de um aplicativo Python, também existe o cffi, que é uma nova alternativa para ctypes . Traz uma nova aparência para a FFI:

  • ele lida com o problema de uma maneira fascinante e limpa (ao contrário dos tipos )
  • não é necessário escrever código não Python (como em SWIG, Cython , ...)
Robert Zaremba
fonte
definitivamente o caminho a percorrer para embrulho , como OP queria. O cython parece ótimo para escrevê-los em loops quentes, mas para interfaces, o cffi simplesmente é uma atualização direta do ctypes.
ovelha voadora
21

Vou jogar outro por aí: SWIG

É fácil de aprender, faz muitas coisas certas e suporta muitos mais idiomas, para que o tempo gasto na aprendizagem possa ser bastante útil.

Se você usa o SWIG, está criando um novo módulo de extensão python, mas com o SWIG fazendo a maior parte do trabalho pesado para você.

Chris Arguin
fonte
18

Pessoalmente, eu escreveria um módulo de extensão em C. Não se deixe intimidar pelas extensões do Python C - elas não são difíceis de escrever. A documentação é muito clara e útil. Quando escrevi pela primeira vez uma extensão C em Python, acho que levei cerca de uma hora para descobrir como escrever uma - não muito tempo.

mipadi
fonte
Quebrando uma biblioteca C. Você pode realmente encontrar o código aqui: github.com/mdippery/lehmer
mipadi
1
@forivall: O código não era realmente tão útil e existem melhores geradores de números aleatórios por aí. Eu só tenho um backup no meu computador.
Mipadi
2
Acordado. A C-API do Python não é tão assustadora quanto parece (supondo que você saiba C). No entanto, ao contrário do python e seu reservatório de bibliotecas, recursos e desenvolvedores, ao escrever extensões em C, você está basicamente sozinho. Provavelmente, sua única desvantagem (além das que normalmente vêm com a escrita em C).
Noob Saibot
1
@mipadi: bem, mas eles diferem entre Python 2.xe 3.x, por isso é mais conveniente usar o Cython para escrever sua extensão, fazer com que o Cython descubra todos os detalhes e compile o código C gerado para o Python 2.x ou 3.x, conforme necessário.
0xC0000022L
2
@mipadi, parece que o link do github está morto e não parece disponível no archive.org, você tem um backup?
jrh 8/04
11

O ctypes é ótimo quando você já possui um blob de biblioteca compilado (como as bibliotecas do sistema operacional). No entanto, a sobrecarga de chamadas é severa; portanto, se você fizer muitas chamadas para a biblioteca e escrever o código C de qualquer maneira (ou pelo menos compilá-lo), eu diria que cython . Não é muito mais trabalhoso, e será muito mais rápido e mais pitônico usar o arquivo pyd resultante.

Pessoalmente, costumo usar o cython para acelerar rapidamente o código python (loops e comparações de números inteiros são duas áreas em que o cython brilha particularmente) e, quando houver algum código / quebra de código envolvido em outras bibliotecas envolvidas, eu voltarei para o Boost. . O Boost.Python pode ser complicado de configurar, mas depois que você o faz funcionar, torna mais fácil a quebra de código C / C ++.

O cython também é ótimo em agrupar numpy (o que aprendi nos procedimentos do SciPy 2009 ), mas como não usei o numpy, não posso comentar sobre isso.

Ryan Ginstrom
fonte
11

Se você já tem uma biblioteca com uma API definida, acho que ctypesé a melhor opção, pois você só precisa fazer uma pequena inicialização e depois chamar mais ou menos a biblioteca do jeito que está acostumado.

Acho que o Cython ou a criação de um módulo de extensão em C (o que não é muito difícil) são mais úteis quando você precisa de um novo código, por exemplo, chamar essa biblioteca e executar algumas tarefas complexas e demoradas, e depois passar o resultado para o Python.

Outra abordagem, para programas simples, é executar diretamente um processo diferente (compilado externamente), produzindo o resultado na saída padrão e chamá-lo com o módulo de subprocesso. Às vezes, é a abordagem mais fácil.

Por exemplo, se você criar um programa de console C que funcione mais ou menos dessa maneira

$miCcode 10
Result: 12345678

Você poderia chamá-lo de Python

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

Com um pouco de formatação de sequência, você pode obter o resultado da maneira que desejar. Você também pode capturar a saída de erro padrão, por isso é bastante flexível.

Khelben
fonte
Embora não haja nada incorreto com esta resposta, as pessoas devem ser cautelosas se o código for aberto para acesso por outras pessoas, pois chamar subprocessos shell=Truepode facilmente resultar em algum tipo de exploração quando um usuário realmente obtém um shell. Tudo bem quando o desenvolvedor é o único usuário, mas no mundo todo há um monte de truques irritantes esperando por algo assim.
Ben
7

Há um problema que me fez usar ctypes e não cython e que não é mencionado em outras respostas.

Usando ctypes, o resultado não depende do compilador que você está usando. Você pode escrever uma biblioteca usando mais ou menos qualquer idioma que possa ser compilado na biblioteca compartilhada nativa. Não importa muito, qual sistema, qual idioma e qual compilador. Cython, no entanto, é limitado pela infraestrutura. Por exemplo, se você deseja usar o compilador Intel no Windows, é muito mais complicado fazer o cython funcionar: você deve "explicar" o compilador para o cython, recompilar algo com esse compilador exato etc. O que limita significativamente a portabilidade.

Misha
fonte
4

Se você estiver direcionando o Windows e optar por agrupar algumas bibliotecas proprietárias do C ++, poderá descobrir em breve que versões diferentes do msvcrt***.dll(Visual C ++ Runtime) são ligeiramente incompatíveis.

Isso significa que você pode não conseguir usar, Cythonpois o resultado wrapper.pydestá vinculado msvcr90.dll (Python 2.7) ou msvcr100.dll (Python 3.x) . Se a biblioteca que você está agrupando estiver vinculada a uma versão diferente do tempo de execução, você estará sem sorte.

Em seguida, para que as coisas funcionem, você precisará criar wrappers C para bibliotecas C ++, vincule a dll do wrapper à mesma versão da msvcrt***.dllsua biblioteca C ++. E, em seguida, use ctypespara carregar sua DLL de invólucro enrolado à mão dinamicamente no tempo de execução.

Portanto, há muitos detalhes pequenos, que são descritos em detalhes no seguinte artigo:

"Bibliotecas nativas bonitas (em Python) ": http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/

iljau
fonte
Esse artigo não tem nada a ver com os problemas que você apresenta com a compatibilidade dos compiladores da Microsoft. Fazer com que as extensões Cython funcionem no Windows não é muito difícil. Consegui usar o MinGW para praticamente tudo. Uma boa distribuição Python ajuda no entanto.
IanH
2
+1 por mencionar um possível problema no Windows (que também estou tendo no momento ...). @IanH é menos sobre janelas em geral, mas é uma bagunça se você estiver preso a uma determinada biblioteca de terceiros que não corresponde à sua distribuição python.
sebastian
2

Eu sei que essa é uma pergunta antiga, mas essa coisa aparece no google quando você pesquisa coisas do tipo ctypes vs cython, e a maioria das respostas aqui são escritas por aqueles que já são proficientes cythonou cque podem não refletir o tempo real que você precisou investir para aprendê-las. para implementar sua solução. Eu sou um novato completo em ambos. Nunca toquei cythonantes e tenho muito pouca experiência c/c++.

Nos últimos dois dias, eu estava procurando uma maneira de delegar uma parte com alto desempenho do meu código para algo mais baixo que o python. Eu implementei meu código em ctypese Cython, que consistia basicamente em duas funções simples.

Eu tinha uma lista enorme de cordas que precisava ser processada. Aviso liste string. Ambos os tipos não correspondem perfeitamente aos tipos c, porque as strings python são por padrão unicode e as cstrings não. Listas em python simplesmente NÃO são matrizes de c.

Aqui está o meu veredicto. Use cython. Ele se integra mais fluentemente ao python e é mais fácil trabalhar com isso em geral. Quando algo der errado, o resultado é ctypesapenas um erro de execução, pelo menos cythonvocê fornecerá avisos de compilação com um rastreamento de pilha sempre que possível, e você poderá retornar facilmente um objeto python válido cython.

Aqui está uma descrição detalhada de quanto tempo eu precisei investir em ambos para implementar a mesma função. Fiz muito pouca programação C / C ++ a propósito:

  • Ctypes:

    • Cerca de 2h em pesquisar como transformar minha lista de strings unicode em um tipo compatível com ac.
    • Aproximadamente uma hora sobre como retornar uma string corretamente da função ac. Aqui, na verdade, forneci minha própria solução para o SO, depois de escrever as funções.
    • Cerca de meia hora para escrever o código em c, compile-o em uma biblioteca dinâmica.
    • 10 minutos para escrever um código de teste em python para verificar se o ccódigo funciona.
    • Cerca de uma hora fazendo alguns testes e reorganizando o ccódigo.
    • Depois, pluguei o ccódigo na base de código real e vi que ctypesnão funciona bem com o multiprocessingmódulo, pois seu manipulador não é selecionável por padrão.
    • Cerca de 20 minutos, reorganizei meu código para não usar o multiprocessingmódulo e tentei novamente.
    • Em seguida, a segunda função no meu ccódigo gerou segfaults na minha base de código, embora tenha passado no meu código de teste. Bem, isso provavelmente é minha culpa por não ter verificado bem com casos extremos, estava procurando uma solução rápida.
    • Por cerca de 40 minutos, tentei determinar as possíveis causas desses segfaults.
    • Dividi minhas funções em duas bibliotecas e tentei novamente. Ainda tinha segfaults para minha segunda função.
    • Decidi deixar de lado a segunda função e usar apenas a primeira função do ccódigo e, na segunda ou terceira iteração do loop python que o usa, eu UnicodeErrornão decodificava um byte em alguma posição, embora codificasse e decodificasse tudo explicitamente.

Nesse ponto, decidi procurar uma alternativa e resolvi procurar cython:

  • Cython
    • 10 min de leitura do cython olá mundo .
    • 15 min de verificação do SO sobre como usar o cython em setuptoolsvez de distutils.
    • 10 min de leitura em tipos de cython e tipos de python. Aprendi que posso usar a maioria dos tipos de python internos para digitação estática.
    • 15 min de re-anotação do meu código python com tipos de cython.
    • 10 min de modificação do meu setup.pypara usar o módulo compilado na minha base de código.
    • Conecte o módulo diretamente à multiprocessingversão do codebase. Funciona.

Para o registro, é claro, não medi os horários exatos do meu investimento. Pode muito bem ser que minha percepção do tempo tenha sido um pouco atenta devido ao esforço mental necessário enquanto eu estava lidando com tipos. Mas deve transmitir a sensação de lidar com cythonectypes

Kaan E.
fonte