Ускоряем Django: View на С

Иногда требуются ресурсоемкие вычисления, и узким местом становится именно питон. Тогда на помощь приходит код написанный на C/С++
К счастью питон-модули пишутся довольно просто, и документация с примерами есть на официальном сайте. Никакой америки я сейчас не открою, и если вы когда-нибудь сталкивались с написанием модуля для питона — далее можно не читать — все довольно примитивно.

Итак, начнем.
Прежде всего понадобится заголовочный файл Python.h и библиотеки для линковки, все это входит в комплект питона или пакета python-dev для debian-ubuntu, так же потребуется компилятор GCC, и пакет distutils для python (он поможет в сборке), впрочем можно обойтись make, но это отдельный разговор.

Итак, модуль питона написанный на C представляет из себя динамическую библиотеку с набором определенных функций.
Это прежде всего функция инициализации, с определенным называнием: init{{MODULENAME}}. Мы назавем наш модуль djc — (django c), поэтому функция инициализации примет вид initdjc. Именно её вызовет интерпретатор python при импорте модуля. И единственное, что должна эта функция обязательно сделать — рассказать питону про загруженный модуль, посредсвом вызова

Py_InitModule("modulename", methods);

где «modulename» — строковое значение имени модуля, а methods — массив из структур PyMethodDef — описывающий методы (функции) модуля. Ну это упрощенно, но на сегодня нам хватит.
На этом обязательная часть закачивается — если модуль кроме загрузки ничего делать не будет, код его будет очень коротким:

#ifdef __cplusplus
extern "C" {
#endif
#include <Python.h>
 
PyMODINIT_FUNC initdjc(void)
{
	printf("-- init module djc --\n");
	Py_InitModule("djc", NULL);
}
#ifdef __cplusplus
}
#endif

При загрузке модуля — вызовется функция initdjc, которая выведет на экран строку, и инициализирует модуль с NULL в качестве экспортируемых им методов.
Для сборки модуля проще всего воспользоваться distutils, который сам выполнит подключение необходимых путей и линковку нужной библиотеки.
Для этого напишем setup.py вида:

from distutils.core import setup, Extension
module1 = Extension('djc',
                    sources = ['djc.c'])
setup (name = 'djc',
       version = '1.0',
       description = 'C Django demo',
       ext_modules = [module1])

и соберем командой:

python setup.py build

Если все библиотеки на месте и код перенесен без ошибок — модуль будет собран в папке build
перейдем в папку с модулем и попробуем выполнить импорт:

>>> import djc
-- init module djc --
>>> help(djc)
Help on module djc:
 
NAME
    djc
 
FILE
    ~/djc/build/lib.macosx-10.8-x86_64-2.7/djc.so
 
(END)

Все так, как и планировалось. Пустой модуль, который при импорте печатает строку в stdout
Теперь добавим функционал, например научим наш модуль возвращать python-строку. Для этого добавим функцию, без параметров, возвращающую строку:

static PyObject * djc_hello(PyObject *self){
	return Py_BuildValue("s", "Hello from C module!");
}

подробнее о Py_BuildValue почитать тут: http://docs.python.org/2/c-api/arg.html#Py_BuildValue

Теперь у нас есть функция и соответственно придется описать её экспорт:

static PyMethodDef ModuleMethods[] = {
	{
		"hello", 
		(PyCFunction)djc_hello,
		METH_NOARGS,
		"Return python string."
	},
	{NULL, NULL, 0, NULL}
};

Что означает какждый параметр можно посмотреть в документации — http://docs.python.org/2/c-api/structures.html#PyMethodDef
Но если кратко:

  1. «hello» — текстовое имя функции, именно по этому имени можно будет её вызывать из питона.
  2. (PyCFunction)djc_hello — ссылка на функцию C которую следует вызывать,
  3. METH_NOARGS — у функции нет аргументов.
  4. текстовое описание функции, его видно в help()

и заменить NULL в djcinit на этот массив струкутр:

PyMODINIT_FUNC initdjc(void)
{
	printf("-- init module djc --\n");
	Py_InitModule("djc", ModuleMethods);
}

Пересобираем, пробуем:

>>> import djc
-- init module djc --
>>> djc.hello()
'Hello from C module!'

В принципе — уже можно начинать использовать в Django, напирмер view.py

def hello(request):
    import djc
    return HttpResponse(djc.hello())

будет отличным образом покажет результат выполение C функции в бразуере.
Но мы пойдем немного дальше — и напишем view полностью на C. Это не сложно. View в Django это функция принимающая 1 параметр (или более) типа django.http.HttpRequest и возвращающая django.http.HttpResponse
Пока обойдемся без входных параметров, просто вернем требующийся HttpReposonse
Для этого, нужно импортировать модуль django.http, найти и вызывать с соответсвующими параметрами в этом модуле объект HttpResponse
Итак, приступим:
Добавим глобальную переменную в которой будет храниться ссылка на загруженный модуль:

PyObject * django_http;

Загружать django.http будем при инициализации нашего модуля, для этого допишем в initdjc импорт:

	django_http = PyImport_Import( 
			PyString_FromString("django.http")
		);

Здесь никакого шаманства — PyImport_Import() получает в качестве параметра Python-строку с именем модуля, и возвращает ссылку на модуль.

И добавим функцию реализующую Django view:

static PyObject * djc_view(PyObject *self, PyObject *args)
{
	PyObject * http_response = PyObject_GetAttrString(django_http, "HttpResponse");	
	PyObject * ag = PyTuple_Pack(1, djc_hello(self));
	return PyObject_CallObject(http_response, ag);
}

Тоже все придельно просто:
Из модуля django.http достаем объект HttpResponse
Собираем параметры запуска — кортеж из одной Python-строки, которую нам вернет наша уже готовая функция djc_hello
Вызываем этот python-объект с нужными нам параметрами и возвращаем результат.

Все — можно пробовать,
привязываем к urls.py:

    url(r'^djc$', 'main.djc.view'),

Запускаем сервер и смотрим в браузере. Работает?

Вместо заключения отправлю читать http://docs.python.org/2/extending/extending.html
И скажу, что так следует поступать, если узким местом действительно является производительность питона.
Нет смысла усложнять себе работу и пытать ускороить тормозной view если python-код отрабатывает за 1% времени, а остальные 99% уходят на выполения запроса к базе.

Ну и полный текст модуля:

#ifdef __cplusplus
extern "C" {
#endif
#include <Python.h>
 
PyObject * django_http;
 
static PyObject * djc_hello(PyObject *self){
	return Py_BuildValue("s", "Hello from C module!");
}
 
 
static PyObject * djc_view(PyObject *self, PyObject *args)
{
	PyObject * http_response = PyObject_GetAttrString(django_http, "HttpResponse");	
	PyObject * ag = PyTuple_Pack(1, djc_hello(self));
	return PyObject_CallObject(http_response, ag);
}
 
 
static PyMethodDef ModuleMethods[] = {
	{
		"view",
		(PyCFunction)djc_view,
		METH_VARARGS,
		"Test Django view"
	},
	{
		"hello",
		(PyCFunction)djc_hello,
		METH_NOARGS,
		"Return python string."
	},
	{NULL, NULL, 0, NULL}
};
 
 
PyMODINIT_FUNC initdjc(void)
{
	printf("-- init module djc --\n");
	Py_InitModule("djc", ModuleMethods);	
	django_http = PyImport_Import( 
			PyString_FromString((char*)"django.http")
		);
}
 
#ifdef __cplusplus
}
#endif