terça-feira, 23 de fevereiro de 2010

Introdução à Python/C API

Nas últimas semanas antes do carnaval eu estava estudando um projeto - RubyPython - o qual tenho grandes intenções em contribuir, pois visa integrar python e ruby com uma interface em Ruby, algo que eu, pessoalmente, acho interessante.
O projeto usa a Python/C API e a Ruby API (ambas em C), assim, eu tive que dar uma olhada nas duas APIs pra poder entender o código.

Já que eu tenho estudado um pouco das duas APIs vou começar escrevendo sobre a API do Python, porque há tempos estou pra aprende-la e até antes do rubypython não tinha tido coragem pra aprender.

Este texto tem como público alvo quem já conhece Python e tem uma base na linguagem C. Será abordado a API da linguagem Python 2.5, que é compatível com as versões 2.4 e 2.6. A API da versão 3 é diferente e não será abordada em nenhum lugar no texto.


Mas afinal, por que fazer em C?

Há duas razões pra usar a Python/C API: escrever módulos de extensão (extension modules) para estender o interpretador Python e embarcar (embed) o interpretador Python (usá-lo como um componente em uma aplicação).

Pode-se entender módulos de extensão como bibliotecas escritas em C/C++ usadas dentro do interpretador Python; e embarcar o interpretador significa usar as facilidades do interpretador a partir da API para escrever código em C ou C++.

Uma das maiores vantagens de usar a Python/C API é sem dúvidas performance. Outra vantagem é poder usar bibliotecas já prontas e adaptá-las pra serem usadas com Python.
Um exemplo de módulo de extensão é a biblioteca PIL.


O básico da Python/C API

Python é uma linguagem dinâmica e nós não temos que nos preocupamos com tipagem estática quando escrevemos código Python. Assim sendo, a API trata todos objetos Python como sendo de um tipo só: um ponteiro para PyObject: PyObject *.

Toda a API está disponível através do cabeçalho Python.h, que normalmente fica em /usr/include/pythonVERSAO, mas você pode descobrir facilmente usando o seguinte comando:

$ python-config --include

No meu caso eu obtive:

hugo@hugo-laptop:~$ python-config --include
-I/usr/include/python2.5 -I/usr/include/python2.5

O -I é a opção que deve ser passada ao GCC para incluir esse diretório nas buscas por cabeçalhos.
Então, antes de tudo nos nossos módulos de extensão ou antes de embarcar o python, devemos escrever o seguinte:

#include "Python.h"

DICA: Evite usar <Python.h> (com <> ao invés de "") pra evitar problemas de multi-plataformas.

Incluir o cabeçalho Python.h implica em incluir também alguns cabeçalhos padrões como: <stdio>, <string.h>, <errno.h>, <limits.h> e <stdlib.h> (se disponíveis).

DICA: Inclua sempre o cabeçalho Python.h antes de qualquer cabeçalho, pois o Python define algumas pre-processor definitions nos cabeçalhos padrões em algumas plataformas.

Todos os "nomes" visíveis através do cabeçalho Python.h começam com Py ou _Py, sendo os que começam com underline usados pra uso interno e devem ser evitados em código de produção.

Sempre que for necessário usar algum recurso como bibliotecas/módulos padrões do interpretador Python é necessário inicializá-lo e após todo o uso finalizá-lo. As funções Py_Initialize e Py_Finalize são respectivamente responsáveis por iniciar e finalizar o interpretador.


Exemplo #1 - embarcando o interpretador: Usando a biblioteca string

Para mostrar um caso do uso de Python embarcado vamos usar a biblioteca padrão string para tornar a string "hugo" em "HUGO" usando o método upper:

#include "Python.h"

int main()
{
PyObject *modulo_string,
*string_maiuscula;
char *string_minuscula = "hugo",
*resultado;
Py_Initialize();

modulo_string = PyImport_ImportModule("string");
string_maiuscula = PyObject_CallMethod(modulo_string, "upper", "s", string_minuscula);
resultado = PyString_AsString(string_maiuscula);
printf("antes: %s, depois: %s\n", string_minuscula, resultado);

Py_Finalize();
return 0;
}

Primeiro nós incluímos o nosso cabeçalho padrão Python.h, depois definimos duas variáveis do tipo PyObject * que serão usadas posteriormente para armazenar o módulo string e o resultado do método upper; em seguida definimos duas variáveis do tipo char * que armazenarão a nossa string "hugo" e o resultado da chamada ao método upper convertido pra C, para que usássemos com a função printf (perceba que não inclui-se <stdio.h> pois está implícito).

Após nossas declarações iniciamos o interpretador Python para que possamos usar uma de suas bibliotecas padrões.

O módulo string é obtivo através da chamada à função PyImport_ImportModule, que importa um módulo (dos módulos definidos em sys.modules).

Depois que já temos o módulo string precisamos chamar o método upper, passando como argumento uma única string (indicada por "s").
Quem nos auxilia nesse caso é a função PyObject_CallMethod, que espera como primeiro parâmetro um objeto python, como segundo o nome do método (tipo char *) e em seguida um formato dos parâmetros (que no nosso caso o "s" significa que haverá em seguida apenas UM parâmetro e que será uma string) e por último passamos o nosso tão aguardado parâmetro pro método upper.

Talvez observando os protótipos destas duas funções os parâmetros fiquem mais claros:

PyObject* PyImport_ImportModule(const char *name)
PyObject* PyObject_CallMethod(PyObject *o, char *method, char *format, ...)

O retorno da chamada a PyObject_CallMethod no código acima é um objeto python que guardará a nossa string em upper case. Porém, a função printf definida em <stdio.h> não conhece os PyObject * da vida. Assim é necessário converter o valor de PyObject * pra char *, usando a função PyString_AsString.
Por fim encerramos o interpretador Python através da função Py_Finalize.

Nós temos o código, mas não temos o executável ainda. Usarei o GCC pra compilar esse código (meu fonte é up.c):

hugo@hugo-laptop:~/pythoncapi$ gcc -o up up.c -I/usr/include/python2.5 -lpython2.5
hugo@hugo-laptop:~/pythoncapi$ ./up
antes: hugo, depois: HUGO

A opção -lpython2.5 foi usada pra linkar a biblioteca do python2.5.

Um pouco trabalhoso, não? Se fosse em Python puro (usando a mesma abordagem) seria apenas:

>>> import string
>>> string_minuscula = "hugo"
>>> resultado = string.upper(string_minuscula)
>>> print resultado
HUGO

Exemplo #2 - estendendo o interpretador: Criando um módulo com funções matemáticas

Módulos de extensão não necessariamente usam as facilidades do interpretador Python, podendo somente criar extensões pro intepretador. Assim, no nosso caso vamos criar apenas duas funções: fatorial e raiz_quadrada e as mesmas serão acessíveis através do módulo matematica.

Para todo módulo de extensão é necessário uma função initNOMEDOMODULO e uma definição de métodos que o módulo conterá. Vamos ao código:

#include "Python.h"
#include <math.h>

int fatorial_c_puro(int n)
{
if (n <= 1)
return 1;
return n * fatorial_c_puro(n - 1);
}

PyObject *raiz_quadrada(PyObject *self, PyObject *n)
{
double raiz = sqrt(PyFloat_AsDouble(n));
return PyFloat_FromDouble(raiz);
}


PyObject *fatorial(PyObject *self, PyObject *n)
{
int n_int = PyInt_AsLong(n);
return PyInt_FromLong(fatorial_c_puro(n_int));
}

PyMethodDef funcoes_matematicas[] = {
{"raiz_quadrada", (PyCFunction)raiz_quadrada, METH_O},
{"fatorial", (PyCFunction)fatorial, METH_O},
{NULL, NULL},
};

void initmatematica()
{
Py_InitModule("matematica", funcoes_matematicas);
}

A primeira função - fatorial_c_puro - foi criada somente pra "limpar" o nosso código e deixar mais simples de entender as chamadas da API e simplesmente retorna o fatorial de um determinado número.

Toda função do tipo: Py_As, como em PyFloat_AsDouble converte do tipo em python pro tipo em C (no caso desta última converte um float em Python pra um double em C). E as funções que ao invés do As possuem o From, como em PyFloat_FromDouble convertem do tipo em C pro tipo em Python.

Toda função que será usada pelo Python tem como primeiro parâmetro um self, que é o contexto/namespace da função. As funções raiz_quadrada e fatorial recebem apenas um parâmetro, porém o self é obrigatório e não conta no número de parâmetros, é como se não existisse na definição da função.

Todo módulo tem uma definição de métodos, que são declarados em um array do tipo PyMethodDef que deve ter como último elemento uma struct com pelo menos NULL e NULL pra indicar o fim do array com as definições. No caso da variável funcoes_matematicas as duas primeiras structs tem como primeiro elemento o nome do método, como segundo a função em C (que usa a Python/C API) que é responsável pelo método, o terceiro é uma macro indicando o número de parâmetros e o quarto e último é a docstring, que nós não colocamos e que por padrão é vazia. No caso onde há apenas um parâmetro usa-se METH_O para evitar parsing manual de parâmetros.

Para facilitar, entenda PyMethodDef como definido da seguinte maneira:

typedef struct {
char *nome;
PyCFunction funcao;
int flags;
char *docstring;
} PyMethodDef;

Vale falar um pouco sobre essa indicação dos parâmetros através de flags. Na API você pode usar algumas indicações de número de parâmetros através de flags, dentre as mais usadas: METH_VARARGS, METH_KEYWORDS, METH_O e METH_NOARGS. Os dois primeiros são bem autoexplicáveis. Neles é necessário fazer o parsing manual dos parâmetros, mesmo que só tenha sido passado um. Pra facilitar nossa vida foi usado METH_O que significa que será recebido apenas um parâmetro.

Outro ponto é o casting pra PyCFunction. O casting foi usado pra evitar warnings do compilador e nada mais.

Como todo módulo precisa de uma função de inicialização do módulo, foi criada a função initmatematica, que dentro dela é inicializado o módulo matematica que contém funcoes_matematicas como os métodos do módulo.

Eu usei o gcc pra gerar o módulo e o python interativo pra mostrar o uso:

hugo@hugo-laptop:~/pythoncapi$ gcc -shared -o matematica.so\
> matematica.c -I/usr/include/python2.5 -lm
hugo@hugo-laptop:~/pythoncapi$ python
Python 2.5.2 (r252:60911, Jan 20 2010, 23:16:55)
[GCC 4.3.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import matematica
>>> matematica.fatorial(5)
120
>>> matematica.raiz_quadrada(2)
1.4142135623730951
>>>

A opção -shared do gcc gera um objeto sem linkage e a -lm é pra linkar a biblioteca matemática.


Referências

Quando eu comecei a estudar a Python/C API foi pela documentação oficial do Python 2.5, o qual eu tinha baixado o fonte. Porém, pelo site docs.python.org é possível acessar a documentação oficial da API da última versão do Python (atualmente 2.6): http://docs.python.org/c-api. Atualmente eu tenho usado o pdf Python/C API Reference Manual pra fazer consultas e o mesmo está acessível também pela página da API da versão 2.5 da linguagem: http://www.python.org/doc/2.5.2/api/api.html.


Esse texto termina aqui, porém tentarei em breve escrever mais sobre a Python/C API e sobre a Ruby API e depois sobre a integração das duas, pra tentar chegar em como o RubyPython funciona.


Até o próximo post ;-)



Updates:
24/02/2010 - O Magno Lima me alertou que eu tinha escrito fibonnaci mas tinha implementado um fatorial. Corrigido!

terça-feira, 2 de fevereiro de 2010

Migrando de Serviço de Blog

Estou largando nesse momento o meu blog na Wordpress: http://hugolt.wordpress.com para usar esse aqui no Blogger.

Motivo da mudança
Eu me cansei de brigar com os htmls no Wordpress, eu gostaria de usar o meu próprio CSS customizado, sem ter que pagar anuidade alguma por isso.
Eu começei a escrever meus posts usando reStructuredText e talvez até use Markdown em algumas situações, pois é mais cômodo que escrever HTML na mão (e a interface do Wordpress pra escrever posts é meio tosca). Eu até criei um CSS parecidíssimo com o que o GitHub usa pra usar nos arquivos markdowns que eu estava escrevendo e o Wordpress não me deixava personalizar o meu blog!
Além disso tudo a interface do Wordpress pra escrever os posts não é lá grandes coisas, é (atualmente) bem lenta comparada a essa do Blogger.

Migração dos Posts
O Blogger não permite importar blogs de outros serviços diferentes do próprio Blogger -diferente do Wordpress - e isso significa que eu não vou migrar todos os posts antigos pra cá.

Futuro do Blog
Espero que com esse novo blog eu blogue mais, pois não tenho mais motivos pra evitar escrever os posts (fora o motivo que sempre tinha: falta de tempo!).

Até o próximo post :-)