Совместное использование 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 :) Приятного чтения, Стефано.