język:

Articles on optimization by Adrian Kalbarczyk

„Połową sukcesu jest umiejętność zadania trafnego pytania”

, by unknown

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++.

Zgłoś błąd na tej stronie
Zgłoś błąd na tej stronie
Here you can let me know about errors on page. Reporting misspelings, grammar errors are very welcomed. If you are experiencing serious site bug please write how it is reproducable. You don't need to fill any of these fields!