Иногда требуются ресурсоемкие вычисления, и узким местом становится именно питон. Тогда на помощь приходит код написанный на 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
Но если кратко:
- «hello» — текстовое имя функции, именно по этому имени можно будет её вызывать из питона.
- (PyCFunction)djc_hello — ссылка на функцию C которую следует вызывать,
- METH_NOARGS — у функции нет аргументов.
- текстовое описание функции, его видно в 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 |