Если вы сталкиваетесь с ограничениями производительности при работе с 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']))
Примеры
В репозитории кода есть три примера блокнотов, вы можете найти их здесь: