Rozszerzanie Pythona o nowe funkcje
Artykuł w oficjalnej dokumentacji Pythona o rozszerzaniu języka jest przegadany pomijając zarazem kilka ważnych rzeczy. Niniejszy tekst opisuje źródło napisane w języku C które po kompilacji rozszerza język Python o bibliotekę md5.
Język Python został zaprojektowany w celu udostępnienia programistom prostego i przejrzystego języka obiektowego będącego nakładką na język C/C++. Zmieniona semantyka sprawia, że na pierwszy rzut oka te języki nie mają ze sobą wiele wspólnego. Owa "nakładkowość" widoczna jest w engine Pythona. Poza dość małą częścią będącą interpreterem składa się on z bibliotek napisanych przede wszystkim w C++, które mają za zadanie udostępnianie funkcji i klas bibliotek napisanych w C/C++ Pythonowi. Wywołując w Pythonie jakąkolwiek funkcję biblioteczną np. md5sum(), wywołujemy tak naprawdę funkcję biblioteczną napisaną w C/C++ - dokładnie tą samą, którą wywołalibyśmy pisząc md5sum() w kodzie C/C++.
Tłumaczenie poleceń Pythona na C/C++ i odpowiednie interpretowanie wyników funkcji C/C++, realizowane jest przez pliki bindujące. Dla przykładu weźmy najmniejszą bibliotekę zamieszczoną w dystrybucji Pythona czyli wspomniany już sumator md5. Kod napisany jest w C i składa się z czterech funkcji:
static void md5_process(md5_state_t *pms, const md5_byte_t *data) void md5_init(md5_state_t *pms) void md5_append(md5_state_t *pms, const md5_byte_t *data, int nbytes) void md5_finish(md5_state_t *pms, md5_byte_t digest[16])
Bibioteka została dostosowana specjalnie na potrzeby dołączenia do Pythona (md5_state_t *pms jest strukturą obiektową). W większości przypadków jednak mamy do czynienia z kodem napisanym z myślą o wykorzystaniu w C/C++ a pisząc moduł udostępniamy go Pythonowi. Należy wtedy poświęcić więcej czasu na opracowanie konstruktora (tu md5_init) i destruktora (tu md5_finish).
Od strony Pythona interfejs obiektu md5 opisany jest na tej stronie. Jak widać biblioteka może zwracać sumy szesnastkowe i dziesiętne. Interfejs powinien pozwalać na wykonanie następującego kodu:
>>> import md5
>>> m = md5.new()
>>> m.update("Nobody inspects")
>>> m.update(" the spammish repetition")
>>> m.digest()
'\xbbd\x9c\x83\xdd\x1e\xa5\xc9\xd9\xde\xc9\xa1\x8d\xf0\xff\xe9'Aby to było możliwe musimy napisać program w C, który po skompilowaniu udostępni Pythonowi odpowiedni moduł (który będzie można importować słowem kluczowym import). Nazwijmy go md5module.c. W pierwszej kolejności dołączamy wymagane nagłówki biblioteki Python.h oraz naszej - md5.h:
#include "Python.h" #include "structmember.h" #include "md5.h"
Biblioteka Python.h udostępnia kilka makr oraz szereg funkcji, które zostaną opisane w dalszej części artykułu. Zadeklarujmy potrzebne struktury:
typedef struct {
PyObject_HEAD
md5_state_t md5; /* the context holder */
} md5object;Struktura zawierająca wymagane makro PyObject_HEAD oraz strukturę klasy md5 z biblioteki md5.h. Makro oznacza, że jest to struktura obiektu a md5_state_t jest definicją atrybutów klasy.
static PyTypeObject MD5type;
Określamy nowy typ. Innymi słowy mówimy, że nasza klasa jest nowego typu (czyli jeszcze nie znanego Pythonowi) o nazwie MD5type. Nazwy tej nie widać nigdzie poza plikiem md5module.c oraz engineem języka - Python nie jest językiem ze ścisłą kontrolą typów. Ponieważ jednak jądro języka jest napisane w C typ musi być jawnie określony.
static md5object *
newmd5object(void)
{
md5object *md5p;
md5p = PyObject_New(md5object, &MD5type);
if (md5p == NULL)
return NULL;
md5_init(&md5p->md5); /* actual initialisation */
return md5p;
}Deklaracja metody. Zalecane nazewnictwo to: nazwa_metody+nazwa_klasy. Ta funkcja jest konstruktorem klasy. Deklarujemy tu strukturę dla atrybutów nowego obiektu i każemy Pythonowi go stworzyć na podstawie tej struktury a następnie ustalić jego typ (PyObject_New()). Jeśli się nie powiedzie - zwraca NULL w przeciwnym wypadku wywoluje funkcję md5_init() z naszej biblioteki i zwraca wskaźnik na strukturę obiektu.
static void
md5_dealloc(md5object *md5p)
{
PyObject_Del(md5p);
}Destruktor obiektu. Python sam zajmuje się niszczeniem obiektów.
static PyObject *
md5_update(md5object *self, PyObject *args)
{
unsigned char *cp;
int len;
if (!PyArg_ParseTuple(args, "s#:update", &cp, &len))
return NULL;
md5_append(&self->md5, cp, len);
Py_INCREF(Py_None);
return Py_None;
}
PyDoc_STRVAR(update_doc,
"update (arg)\n\
\n\
Update the md5 object with the string arg. Repeated calls are\n\
equivalent to a single call with the concatenation of all the\n\
arguments.");Standardowa deklaracja metody. Dokumentacja języka odbywa się już na poziomie pliku bindującego. Py_None to Pythonowy obiekt None. Nie mylić z null!
static PyObject *
md5_copy(md5object *self)
{
md5object *md5p;
if ((md5p = newmd5object()) == NULL)
return NULL;
md5p->md5 = self->md5;
return (PyObject *)md5p;
}Konstruktor kopiujący.
static PyMethodDef md5_methods[] = {
{"update", (PyCFunction)md5_update, METH_VARARGS, update_doc},
{"digest", (PyCFunction)md5_digest, METH_NOARGS, digest_doc},
{"hexdigest", (PyCFunction)md5_hexdigest, METH_NOARGS, hexdigest_doc},
{"copy", (PyCFunction)md5_copy, METH_NOARGS, copy_doc},
{NULL, NULL} /* sentinel */
};Tablica md5_methods opisuje metody klasy w formacie: mazwa w Pythonie, nazwa w C, makro opisujące argumenty metody, dokumentacja. Tabica musi kończyć się elementem {NULL, NULL}. Jest to znak końca tablicy.
static PyObject *
md5_get_block_size(PyObject *self, void *closure)
{
return PyInt_FromLong(64);
}
static PyObject *
md5_get_digest_size(PyObject *self, void *closure)
{
return PyInt_FromLong(16);
}
static PyObject *
md5_get_name(PyObject *self, void *closure)
{
return PyString_FromStringAndSize("MD5", 3);
}
static PyGetSetDef md5_getseters[] = {
{"digest_size",
(getter)md5_get_digest_size, NULL,
NULL,
NULL},
{"block_size",
(getter)md5_get_block_size, NULL,
NULL,
NULL},
{"name",
(getter)md5_get_name, NULL,
NULL,
NULL},
/* the old md5 and sha modules support 'digest_size' as in PEP 247.
* the old sha module also supported 'digestsize'. ugh. */
{"digestsize",
(getter)md5_get_digest_size, NULL,
NULL,
NULL},
{NULL} /* Sentinel */
};Definicje specyficznych dla Pythonowych obiektów metod tj. get i set. Wywoływane są one kiedy zewnętrzny obiekt zmienia atrybuty obiektu. Można w ten sposób zakazać modyfikowania czyli zrobić zmienne prywatne. Tablica kończy się elementem NULL.
Następne linie odpowiadają za dokumentację modułu oraz typu po czym następuje kod zbierający wszystkie struktury i tworzący klasę:
static PyTypeObject MD5type = {
PyObject_HEAD_INIT(NULL)
0, /*ob_size*/
"_md5.md5", /*tp_name*/
sizeof(md5object), /*tp_size*/
0, /*tp_itemsize*/
/* methods */
(destructor)md5_dealloc, /*tp_dealloc*/
0, /*tp_print*/
0, /*tp_getattr*/
0, /*tp_setattr*/
0, /*tp_compare*/
0, /*tp_repr*/
0, /*tp_as_number*/
0, /*tp_as_sequence*/
0, /*tp_as_mapping*/
0, /*tp_hash*/
0, /*tp_call*/
0, /*tp_str*/
0, /*tp_getattro*/
0, /*tp_setattro*/
0, /*tp_as_buffer*/
Py_TPFLAGS_DEFAULT, /*tp_flags*/
md5type_doc, /*tp_doc*/
0, /*tp_traverse*/
0, /*tp_clear*/
0, /*tp_richcompare*/
0, /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
md5_methods, /*tp_methods*/
0, /*tp_members*/
md5_getseters, /*tp_getset*/
};Komentarze wyjaśniają do czego służą poszczególne pola. Ważne jest, że liczba pól jest stała. W tak prostym module większość pól jest pusta. Bardzo rzadko potrzebujemy wypełnić wszystkie.
static PyObject *
MD5_new(PyObject *self, PyObject *args)
{
md5object *md5p;
unsigned char *cp = NULL;
int len = 0;
if (!PyArg_ParseTuple(args, "|s#:new", &cp, &len))
return NULL;
if ((md5p = newmd5object()) == NULL)
return NULL;
if (cp)
md5_append(&md5p->md5, cp, len);
return (PyObject *)md5p;
}
static PyMethodDef md5_functions[] = {
{"new", (PyCFunction)MD5_new, METH_VARARGS, new_doc},
{NULL, NULL} /* Sentinel */
};Jeśli chcemy udostępnić funkcję, deklarujemy ją a następnie, w podobny sposób jak z metodami, dopisujemy ją do tablicy md5_functions.
Do automatyzacji tego procesu powstało kilka narzędzi. Są one jednak na tyle niepraktyczne, że prostrze jest przystosowanie powyższego pliku do własnych potrzeb.
Wkompilowanie biblioteki do Pythona realizuje poniższy skrypt:
from distutils.core import setup, Extension
module1 = Extension('demo',
sources = ['md5module.c'])
setup (name = 'MD5',
version = '1.0',
description = 'This is MD5 module',
ext_modules = [md5module])Kod ten wyprodukuje moduł w bierzącym katalogu. Możemy go użyć imprtując md5module w kodzie programu Pythonowego. Aby moduł był widoczny globalnie przez Pythona wygenerowaną biblioteke należy przekopiować do odpowiedniego katalgu z modułami.
Istota Pythona polego na bindowaniu bibliotek napisanych w C/C++. W Pythonie pisze się te kawałki kodu, które wywoływane są rzadko lub optymalizacja nie jest konieczna. Dobrym przykładem wykorzystania Pythona jest pisanie w nim interfejsów graficznych. QT ma jedne z lepszych bindingów dla Pythona. Wszystkie czasochłonne operacje wykonują się w bibliotekach napisanych w C++.

