Existem várias formas de integrar códigos escritos em C/C++ com nosso código em Python. A principal motivação para fazê-lo é a necessidade de rapidez na execução de algumas rotinas, sobretudo as matemáticas.
Conforme já sabemos, Python é uma linguagem fantástica por ser muito fácil de aprender, versátil e com uma comunidade muito grande.
No entanto, como nada é perfeito, existe um grande déficit de performance de execução. Felizmente, a linguagem fornece vários meios de integração com outras linguagens, como por exemplo C/C++.
Desta forma, visto que a linguagem C/C++ nesse aspecto leva muita vantagem frente a Python, podemos contornar tranquilamente esse problema.
Em síntese, veremos resumidamente ao decorrer deste artigo formas diferentes de fazer essa integração, desde as mais fáceis e não tão performáticas até as mais complexas e rápidas. Assim você poderá escolher aquela que melhor se encaixa às suas necessidades.
O Problema em Python
Primeiramente imagine uma simples função em Python que conta os número primos de um dado intervalo numérico:
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def count_primes(limit):
count = 0
for num in range(2, limit):
if is_prime(num):
count += 1
return count
Note que essa função foi propositadamente criada de forma ineficiente para que sobrecarregue a CPU.
Em seguida imagine que queremos saber quantos números primos existem entre 0 e 1000000 e quanto tempo isso leva:
import time
limit = 10**6 # Ajuste para um número maior para mais carga de CPU
start_time = time.time()
prime_count = count_primes(limit)
end_time = time.time()
print(f"Números primos até {limit}: {prime_count}")
print(f"Tempo de execução: {end_time - start_time:.2f} segundos")
Evidentemente o tempo de execução varia de acordo com o hardware e o momento da execução. No meu caso o tempo foi de 3.87 segundos.
Por fim vejamos como podemos melhorar o tempo de execução deste código ao reescrevê-lo em C/C++. Em primeiro lugar vamos à preparação do ambiente.
Compilador C/C++
Para que fique mais fácil, faremos a compilação dos códigos C/C++ utilizando o compilador G++, muito comum em sistemas Linux. Antes de tudo, verifique se o compilador está instalado com o comando:
g++ --version
Caso não esteja presente por qualquer razão, basta instalá-lo com o apt: (em distribuições Debian)
apt install g++
Usuários Windows podem instalar o G++ por meio do MinGW-w64. Nesse sentido acesse o site do MSYS2 e siga o passo a passo para instalação do MSYS2 e do MinGW. Como resultado, você estará pronto para executar o comando G++ igual ao Linux.
Além disso, outra opção para usuários mais avançados é a utilização do G++ dentro do WSL do Windows.
Uma vez instalado o compilador, vejamos a versão em C/C++ do nosso contador de números primos escrito originalmente em Python:
#include <iostream>
#include <cmath>
#include <chrono>
extern "C" {
bool is_prime(int n) {
if (n < 2) return false;
for (int i = 2; i <= std::sqrt(n); ++i) {
if (n % i == 0) return false;
}
return true;
}
int count_primes(int limit) {
int count = 0;
for (int num = 2; num < limit; ++num) {
if (is_prime(num)) {
++count;
}
}
return count;
}
}
Crie no local de sua preferência um arquivo chamado primes.c e cole o conteúdo acima
Por fim, vamos compilar em formato de biblioteca a versão em C/C++ do nosso código contador de números primos com o seguinte comando:
g++ -x c++ -shared -o libprimes.so -fPIC primos.c
O compilador irá gerar a biblioteca libprimes.so que precisará estar na mesma pasta do projeto Python que a utilizará.
Opções de Integração C/C++ com Python
Existem várias formas de integrar códigos C/C++ e Python, seja através de subprocessos (através do executável do programa), por meio de Shared Objects (arquivo .so do Linux ou .dll do Windows) ou até mesmo via rede por API REST.
A maneira mais comum, no entanto, é a integração utilizando o código C/C++ compilado como biblioteca, os chamados Shared Objects. Além de melhorar a performance, proporciona uma melhor comunicação entre os programas.
Nesse sentido, Python nos oferece algumas bibliotecas que ajudam a integração entre as linguagens, como por exemplo ctypes, cffi e pybind11
Além disso, existe uma forma híbrida onde um determinado código Python é convertido diretamente para C/C++, conhecido como Cython.
Portanto, vejamos a seguir cada uma delas.
Integrando com ctypes
O ctypes é uma biblioteca nativa do Python que funciona bem para chamar funções C de uma biblioteca .so/.dll. Porém ela não suporta classes C++, apenas funções C.
Trata-se da forma mais simples de integrar um código C/C++ com Python, porém é a mais lenta. Portanto, se você não precisa realizar diversas chamadas à função, pode ser uma opção viável.
import ctypes
# Carregar a biblioteca compartilhada (substitua 'libprimes.so' pelo nome correto no seu sistema)
lib = ctypes.CDLL('./libprimes.so') # Linux/Mac
# lib = ctypes.CDLL('primes.dll') # Windows
# Definir o tipo de retorno e argumento da função count_primes
# O nome da função definido aqui deve ser o mesmo da função C/C++
lib.count_primes.argtypes = [ctypes.c_int]
lib.count_primes.restype = ctypes.c_int
# Chamar a função e exibir o resultado
limit = 10**6
start_time = time.time()
prime_count = lib.count_primes(limit)
end_time = time.time()
print(f"**C/C++** Números primos até {limit}: {prime_count}")
print(f"**C/C++** Tempo de execução: {end_time - start_time:.2f} segundos")
No meu caso, o tempo de execução utilizando C/C++ com ctypes foi de 0,49 segundos, contra os 3,87 do código em Python.
Integrando com cffi
Em comparação ao ctypes, o cffi é uma biblioteca mais flexível pois suporta ponteiros e structs. Além disso, permite compilar código C/C++ embutido no código Python. Em contrapartida, ainda não suporta diretamente classes C/C++.
Trata-se de uma biblioteca externa, portanto precisa de instalação via pip:
pip install cffi
A criação do código que chama nosso contador de números primos é bastante simples:
from cffi import FFI
# O objeto FFI é responsável por carregar a biblioteca .so
# e definir a assinatura do método
ffi = FFI()
lib = ffi.dlopen("./libprimes.so")
# Windows
# lib = ffi.dlopen("./libprimes.dll")
ffi.cdef("int count_primes(int);")
# Chamar a função e exibir o resultado
limit = 10**6
start_time = time.time()
prime_count = lib.count_primes(limit)
end_time = time.time()
print(f"**cffi** Números primos até {limit}: {prime_count}")
print(f"**cffi** Tempo de execução: {end_time - start_time:.2f} segundos")
Como resultado, a execução desse código levou os mesmos 0,49 segundos do método anterior (ctypes)
Integrando com pybind11
O pybind11 é uma biblioteca bem mais difícil se utilizar quando comparada ao ctype e ao cffi, no entanto possui vantagens consideráveis. Além de apresentar uma integração mais natural, provê suporte a classes, templates e sobrecarga, sendo ótima para projetos C/C++ grandes.
Primeiramente, precisamos instalá-la:
pip install pybind11
Nessa abordagem precisamos modificar um pouco nosso código C/C++ de forma que, desta vez, invertemos os papéis: o contador de números primos é quem define a assinatura da função em Python. Um pouco confuso, mas o código abaixo esclarece um pouco:
#include <pybind11/pybind11.h>
bool is_prime(int n) {
if (n < 2) return false;
for (int i = 2; i <= std::sqrt(n); ++i) {
if (n % i == 0) return false;
}
return true;
}
int count_primes(int limit) {
int count = 0;
for (int num = 2; num < limit; ++num) {
if (is_prime(num)) {
++count;
}
}
return count;
}
# Aqui definimos como nosso programa Python acessará o método
# count_primes
PYBIND11_MODULE(primes, m) {
m.def("count_primes", &count_primes, "Contador de números primos");
}
Uma vez que modificamos o código, precisamos compilá-lo novamente. Desta vez, de uma forma um pouco diferente:
g++ -O3 -Wall -shared -std=c++11 -fPIC $(python3 -m pybind11 --includes) primes.cpp -o primes.so
O comando acima gera um novo arquivo de biblioteca compartilhada (.so) que utilizaremos através do import em Python. Caso esteja usando Windows, apenas modifique o final do comando de primes.so para primes.dll.
import primes
import time
limit = 10**6 # Ajuste para um número maior para mais carga de CPU
start_time = time.time()
prime_count = primes.count_primes(limit)
end_time = time.time()
print(f"**pybind11** Números primos até {limit}: {prime_count}")
print(f"**pybind11** Tempo de execução: {end_time - start_time:.2f} segundos")
Note que a utilização em Python torna-se muito natural de modo que sequer sabemos que a biblioteca primes foi escrita em C/C++. Desta vez o resultado da execução, foi de 0,22 segundos, ainda melhor que as formas anteriores.
Integrando com cython
O cython é uma biblioteca cuja abordagem difere ainda mais das outras apresentadas até agora. Embora tenha o mesmo objetivo de utilizar bibliotecas compiladas C/C++, ela dispensa a necessidade de programar diretamente em C/C++.
Nesse sentido, ela converte um código Python com “tipagem” C/C++ para a própria linguagem C/C++ e o compila. A princípio um pouco confuso, no entanto o exemplo abaixo torna mais claro o conceito.
Antes de mais nada, vamos instalar o cython:
pip install cython
No lugar do nosso código nativo C/C++, damos lugar ao código “híbrido” em Python que deverá ser salvo com a extensão .pyx:
# n está agora "tipado" como int
def is_prime(int n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
# n está agora "tipado" como int
def count_primes(int limit):
count = 0
for num in range(2, limit):
if is_prime(num):
count += 1
return count
Note que apenas as definições das funções is_prime e count_primes foi alterada incluindo o tipo int da linguagem C/C++.
Agora precisamos de um script Python que faça a conversão desse código para C/C++ e o compile como biblioteca:
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules=cythonize("primes.pyx")
)
Desse modo podemos executá-lo da seguinte forma:
python setup.py build_ext --inplace
Como resultado, teremos o arquivo Shared Object (.so) ou .dll no caso do Windows pronto para uso diretamente no nosso programa Python:
import primes
import time
limit = 10**6 # Ajuste para um número maior para mais carga de CPU
start_time = time.time()
prime_count = primes.count_primes(limit)
end_time = time.time()
print(f"**cython** Números primos até {limit}: {prime_count}")
print(f"**cython** Tempo de execução: {end_time - start_time:.2f} segundos")
Repare que esse código é exatamente igual a nossa versão anterior construída com pybind11. É uma solução igualmente natural uma vez que não sabemos dos detalhes de implementação da biblioteca primes.
Em contrapartida, neste caso cython demonstrou pior desempenho que as demais, 2,64 segundos.
Conclusão
Embora possua como desvantagem o aspecto da velocidade, Python honra sua versatilidade e flexibilidade oferecendo várias opções de integração com linguagens mais rápidas como é o caso de C/C++.
Além disso, dentre as várias opções, podemos escolher aquela melhor se encaixa às necessidades do projeto. Precisa de uma integração simples? Utilize ctypes ou cffi. Precisa de velocidade e integração com projetos grandes? Vá de pybind11. Não sabe programar em linguagem C/C++? Use cython.
Vale ressaltar que ainda existem mais opções de integração não citadas aqui como por exemplo o SWIG. Do mesmo modo, existem mecanismos de integração com várias outras linguagens, como Rust, Java, C#, Go, R, Node.js e outras.
Espero ter ajudado!
Até a próxima!