Совместное использование Python и C++ с помощью библиотеки Pybind.
Краткое содержание
В этом эпизоде мы собираемся использовать массивы numpy и списки python для обработки данных, которые будут переданы некоторым функциям C++ для выполнения дополнительных вычислений.
Обработать некоторые данные
Возьмем в качестве примера функцию ProcessSomeData, которая вычисляет квадрат входного массива размером N (указывается pIn) и сохраняет результат в выходной буфер (указанный pOut).
void ProcessSomeData(const float* pIn, float* pOut, const size_t N) { for (size_t i = 0; i < N; i++) { pOut[i] = pIn[i] * pIn[i]; } }
Наивная реализация
Как и в других примерах, мы должны написать что-то вроде этого:
PYBIND11_MODULE(example, m) { m.doc() = "pybind11 example"; m.def("process_some_data", &ProcessSomeData); }
и пусть PyBind11 творит чудеса. В Python, используя массивы numpy, я напишу:
import example import numpy as np input_array = np.array([1,2,3,4], dtype=np.float32) output_array = np.array([0,0,0,0], dtype=np.float32) samples = 4 example.process_some_data(input_array, output_array, samples)
К сожалению, это вызовет ошибку:
example.process_some_data(a, b, samples) TypeError: process_some_data(): incompatible function arguments. The following argument types are supported: 1. (arg0: float, arg1: float, arg2: int) -> None Invoked with: array([1., 2., 3., 4.], dtype=float32), array([0., 0., 0., 0.], dtype=float32), 4
Использование py::array_t
К счастью, pybind11 имеет оболочку py::array_t‹T›, которая позволяет преобразовать массив NumPy в массив c style. Итак, давайте изменим функцию m.def(“process_some_data”, &ProcessSomeData) на лучшую (не забудьте #include ‹pybind11/numpy.h›) :
m.def("process_some_data", [](const py::array_t<float> &input, py::array_t<float>& out) { py::buffer_info input_buffer = input.request(); py::buffer_info out_buffer = out.request(); auto *ptr1 = static_cast<float *>(input_buffer.ptr); auto *ptr2 = static_cast<float *>(out_buffer.ptr); auto samples = input_buffer.shape[0]; ProcessSomeData(ptr1, ptr2, samples); });
Здесь я объявил лямбду, которая принимает в качестве аргументов два py::array_t‹float› и вызывает .request() я могу получить доступ к структуре py::buffer_info, которая дает мне прямой доступ к указателю (типа void), который я приводил к указателю типа плавающий (ptr1, ptr2). Вызов .shape[0] дает мне длину массива. Вот подробный вид структуры buffer_info:
struct buffer_info { void *ptr = nullptr; // Pointer to the underlying storage ssize_t itemsize = 0; // Size of individual items in bytes ssize_t size = 0; // Total number of entries std::string format; // format ssize_t ndim = 0; // Number of dimensions std::vector<ssize_t> shape; // Shape of the tensor (1 entry per dim.) std::vector<ssize_t> strides; // strides bool readonly = false; ....
Давайте снова запустим скрипт Python. На этот раз работает как положено:
import example import numpy as np samples = 4 input_array = np.array([1,2,3,4], dtype=np.float32) output_array = np.array([0,0,0,0], dtype=np.float32) example.process_some_data(input_array, output_array) print(input_array) print(output_array) [1. 2. 3. 4.] [1. 4. 9. 16.]
Короткий перерыв…
Использование контейнеров STL
Рассмотрим теперь функцию, которая принимает два аргумента std::vector‹float› в качестве аргументов, передаваемых по ссылке, и записывает соответствующую привязку (не забудьте #include ‹pybind11/stl.h› файл, иначе вы получите ошибку).
void ProcessSomeDataII(const std::vector<float>&input, std::vector<float>& output) { for (size_t i = 0; i < input.size(); i++) { output[i] = input[i] * input[i]; } } PYBIND11_MODULE(example, m) { m.doc() = "pybind11 example"; m.def("process_some_dataII", ProcessSomeDataII); }
Давайте попробуем снова запустить скрипт python, на этот раз используя два списка python input_array и output_array:
input_array = [1,2,3,4] output_array = [0,0,0,0] example.process_some_dataII(input_array, output_array) print(input_array) print(output_array) [1, 2, 3, 4] [0, 0, 0, 0]
Мы ожидали увидеть b = [1, 4, 9, 16], значит, что-то работает не так. Из мануала pybind11 читаем: […. pybind11 создаст новый std::vector‹int› и скопирует каждый элемент из списка Python. То же самое происходит и в другом направлении: создается новый список, соответствующий значению, возвращенному из C++…].
Итак, здесь мы обнаружили проблему: std::vector копируются и не передаются по ссылке. Чтобы это произошло, мы должны переопределить автоматическое преобразование пользовательской оболочкой (т. е. упомянутый выше подход 1). Это требует некоторой ручной работы, и дополнительные сведения доступны в разделе Создание непрозрачных типов.
Обходной путь
Прежде чем говорить об непрозрачных типах, первым решением будет переписать функцию, чтобы она возвращала std::vector‹float›. Это означает, что мы сталкиваемся с двумя операциями копирования:
- Первая копия из input_array в std::vector‹float›input
- Вторая копия из std::vector‹float›output в output_array
std::vector<float> ProcessSomeDataIII(const std::vector<float>&input) { std::vector<float>output; output.resize(input.size()); for (size_t i = 0; i < input.size(); i++) { output[i] = input[i] * input[i]; } return output; } m.def("process_some_dataIII", ProcessSomeDataIII);
В python вызов новой функции будет работать так, как ожидалось:
input_array = [1,2,3,4] output_array = [0,0,0,0] output_array = example.process_some_dataIII(input_array) print(input_array) print(output_array) [1, 2, 3, 4] [1.0, 4.0, 9.0, 16.0]
Как уже было сказано, это работает, но помните, что операции копирования обходятся дорого и приводят к неэффективному коду, особенно для больших массивов.
На данный момент мы хотели бы найти другую лучшую стратегию для управления списками python и объектами std::vector. Ответ заключается в том, чтобы сделать типы непрозрачными, чтобыразрешить передачу по ссылке операции, но мы поговорим об этом в следующем рассказе. На этом пока все, в следующий раз мы увидим другие интересные примеры.
Итак, если вам нравится этот контент, подписывайтесь на меня на Medium :) Приятного чтения, Стефано.