Cython'им fastapi-проект: сравниваем скорость pure python и cython имплементаций
Что такое fastapi?
fastapi - имхо, самый лучший фреймворк для веб-разработки на Python, основными фичами которого являются:
- высокая производительность относительно других фреймворков на Python за счет использования starlette и uvicorn в качестве ASGI-сервера, сравним по скорости с фреймворками на Go и NodeJS
- простота использования - благодаря pydantic (для валидации запросов) и целой куче готовых подмодулей для любых задач (websockets, background tasks, система зависимостей компонентов приложения, middlewares и многое другое) очень легко и интуитивно понятно, как написать какую-то вещь
- полная типизация фреймворка и поддержка редакторами - меньше времени на дебаг и подсказки IDE
- автоматическая генерация документации (по схеме OpenAPI, Swagger и Redoc)
Что такое cython?
Cython - это промежуточный слой между Python и C/C++. Cython позволяет писать обычный Python-код, добавляя функции из стандаратной библиотеки языков C/C++ и ускоряя код за счет использования C-типов (что, впрочем, не обязательно), который затем напрямую транслируется в C-код.
Что будем делать?
В этом блоге попробуем ускорить и без того довольно быстрый проект на fastapi, ситонизируя (cythonize) его. Для примера возьмем вот такой код (выдрал минимальный код из одного из текущих проектов, поэтому такая структура):
from fastapi import APIRouter
router = APIRouter(
prefix="/ftl.",
tags=["ftl"]
)
@router.get("test")
async def test_method():
return {"ping": "pong"}
from .ftl import ftl
from fastapi import FastAPI
import uvicorn
from routers import ftl
app = FastAPI()
app.include_router(ftl.router, prefix="/method")
@app.get("/")
async def root():
return {"message": "Hello Bigger Applications!"}
uvicorn.run("main:app", host="127.0.0.1", port=5000, log_level="critical")
Тут мы создаем роутер для группы методов под названием ftl
с одним методом - ftl.test
.
Обычно для запуска fastapi-приложений используют uvicorn из терминала (примерно так):
python3 -m uvicorn main:app --host 0.0.0.0 --port 5000
Но так как мы скомпилируем проект в один бинарник, который можно будет запускать через ./app
, запуск ASGI-сервера мы засунули в код проекта.
Теперь установим нужные зависимости (желательно в виртуальном окружении):
pip3 install fastapi
pip3 install cython
pip3 install 'uvicorn[standard]'
Обратите внимание, что мы ставим не обычный uvicorn
, а uvicorn[standart]
- его ситонизированную версию. Это даст некоторую прибавку в скорости даже при запуске pure python кода.
Ситонизируем и оцениваем!
Чтобы ситонизировать наш проект, потребуется написать олдскульный файл setup.py
:
from setuptools import setup
from Cython.Build import cythonize
setup(
ext_modules = cythonize("main.pyx")
)
Теперь напишем builder.c
- файл для генерации единого бинарника со всем проектом:
#include <Python.h>
int main(int argc, char *argv[]) {
// Инициализация интерпретатора Python
Py_Initialize();
// Вызов вашей функции из скомпилированной библиотеки
PyObject *pName, *pModule, *pDict, *pFunc, *pValue;
pName = PyUnicode_DecodeFSDefault("main.cpython-310-x86_64-linux-gnu");
pModule = PyImport_Import(pName);
pDict = PyModule_GetDict(pModule);
pFunc = PyDict_GetItemString(pDict, "root");
pValue = PyObject_CallObject(pFunc, NULL);
// Вывод результата или обработка его каким-либо образом
// Завершение интерпретатора Python
Py_Finalize();
return 0;
}
Да, он написан ChatGPT. Я не умею в Python API 😢
Собираем проект в единый бинарь:
cython main.pyx
python3 setup.py build_ext --build-lib=. # создаст файл main.c и скомпилит его в .so
gcc -o app.o builder.c -I/usr/include/python3.10 -lpython3.10 -Wall -g # компилим main.c в обычный бинарь (.o)