Если вы сталкиваетесь с ограничениями производительности при работе с Python, пришло время подумать о том, чтобы разделить часть вашей рабочей нагрузки на модуль C/C++ с использованием расширений Python.

Под капотом Python построен на языке C, а это означает, что многие популярные библиотеки имеют быстрые реализации C общих алгоритмов. Для сложных задач неэффективность Python может привести к неработоспособным решениям. Для повышения скорости и надежности часть вашей рабочей нагрузки можно преобразовать в C++.

Подождите, разве это не много работы?

Я так не думаю. Это зависит от рабочей нагрузки и того, почему вы в первую очередь рассматриваете возможность использования C/C++.

Помните, что вы не пытаетесь перенести всю кодовую базу на C. Большая часть кода, вероятно, должна остаться на Python.

Для своей работы по оптимизации я использую шаблон сэндвича:

  • Python: загрузка данных, очистка
  • C/C++: подгонка модели, числовая интеграция и т. д.
  • Python: визуализация результатов

Эта установка дает нам лучшее из обоих миров. Python отлично подходит для загрузки данных с помощью таких инструментов, как pandas, а Jupyter — лучшее место для визуализации и изучения результатов. Использование C++ для тяжелой работы означает, что у нас есть полный контроль над выбором и реализацией алгоритма. Мы также можем гарантировать кросс-платформенную переносимость в любую среду, в которой работает этот модуль C++.

Для создания расширения Python создаются функции-оболочки, определяющие API модуля. Эти функции компилируются вместе с исходным кодом приложения для создания модуля, который затем можно импортировать в среду Python.

Перейдите прямо к коду здесь: https://github.com/parameter-estimation/ambient-temperature-estimation/tree/main/src/apis/python

В проекте Ambient Temperature Estimator мы определяем расширение как модуль со следующим API (python_wrapper.cpp):

static PyMethodDef module_methods[] = {
        {"init", init_wrapper, METH_VARARGS, init_docstring},
        {"feed", feed_wrapper, METH_VARARGS, feed_docstring},
        {"fit", fit_wrapper, METH_VARARGS, fit_docstring},
        {"generate", generate_wrapper, METH_VARARGS, generate_docstring},
        {"version", version_wrapper, METH_VARARGS, version_docstring},
        {NULL, NULL, 0, NULL}
};

Каждый метод, определенный для модуля, должен иметь имя, вызываемую функцию и строку документации.

Ввод данных пользователем

Чтобы получить данные, переданные в C из Python, проанализируйте аргументы с помощью PyArg_ParseTuple. Вот пример функции, которая принимает два параметра: число и список чисел. Оболочка копирует этот список в std::vector и затем вызывает целевую функцию.

static char feed_docstring[] = "Feed data to optimizer";
static PyObject *feed_wrapper(PyObject *self, PyObject *args)
{
    double t;
    PyObject *float_list;
    if (!PyArg_ParseTuple(args, "dO", &t, &float_list)) {
        return nullptr;
    }

    int n = PyObject_Length(float_list);

    std::vector<double> x(n);
    for (int ii=0; ii<n; ii++) {
        PyObject *item;
        item = PyList_GetItem(float_list, ii);
        x[ii] = PyFloat_AsDouble(item);
    }

    optimizer.feed(t,&x);
    Py_RETURN_NONE;
}

Вывод

Чтобы вернуть данные из C в Python, я предпочитаю возвращать вещи внутри словаря, чтобы можно было использовать имена ключей и значений. Вот пример функции, которая возвращает вложенный словарь:

static char fit_docstring[] = "Fit the data given";
static PyObject *fit_wrapper(PyObject *self, PyObject *args)
{

    optimizer_result_t  result = optimizer.fit();

    PyObject *fitted_params_object = Py_BuildValue("{s:d,s:d,s:d,s:d}",
                                                   "h", result.fitted_params.h,
                                                   "q", result.fitted_params.q,
                                                   "T_dev_0", result.fitted_params.T_dev_0,
                                                   "T_amb_0", result.fitted_params.T_amb_0);

    return Py_BuildValue("{s:d,s:d,s:i,s:i,s:O}",
                         "is_valid", (double)result.is_valid,
                         "rmse", result.rmse,
                         "icount", result.icount,
                         "ifault", result.ifault,
                         "fitted_params", fitted_params_object);
}

Создание модуля

Когда реализация модуля будет готова, мы скомпилируем ее с помощью специального Python-скрипта setup.py. Этот сценарий использует distutils.core для определения и сборки расширения Python. Здесь мы определяем список исходных кодов C, которые будут скомпилированы, пути включения заголовков и любые необходимые нам зависимости, в данном случае NLopt и NumPy.

from distutils.core import setup, Extension
import numpy

extra_objects = ['/usr/local/lib/libnlopt.dylib']
libraries = ['nlopt']

c_ext = Extension(
    "ambient_optimizer_python_api",
    sources=[
        "python_wrapper.cpp",
        "../optimizer_api.cpp",
        "../../optimizer/optimizer_data_buffer.cpp",
        "../../optimizer/optimizer_objective_functions.cpp",
        "../../optimizer/optimizer_fitting.cpp",
        "../../optimizer/optimizer_solver.cpp",
        "../../util/utils.cpp"
    ],
    libraries=libraries,
    extra_objects=extra_objects,
    extra_compile_args=[
        "-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION",
        "-std=c++17"
    ]
)

setup(
    name='ambient_optimizer_python_api',
    version='1.0.0',
    ext_modules=[c_ext],
    include_dirs=[
        "../../headers",
        "../../models",
        numpy.get_include()
    ]
)

Использование

После того, как вы скомпилировали модуль, вы можете импортировать его из Python и взаимодействовать с вашими API. Вот фрагмент, который импортирует модуль C++, загружает некоторые данные (с помощью Python), отправляет данные в модуль и возвращает результат обратно в Python.

import ambient_optimizer_python_api as aopa
train_data = pd.read_csv("train.csv")

# Train model
aopa.init({"model": "train"})
for t, T_dev in enumerate(train_data['Tdev']):
    aopa.feed(t, [T_dev, train_data['Tamb'][t]])

fit_result = aopa.fit()
print("fit result: h={} q={}".format(fit_result['fitted_params']['h'], fit_result[fitted_params['q']))

Примеры

В репозитории кода есть три примера блокнотов, вы можете найти их здесь:

https://github.com/parameter-estimation/ambient-temperature-estimation/tree/main/src/apis/python/examples