В настоящее время я рассматриваю сторонний пакет Python с привязками, использующими cffi для доступа к инструментам прогнозирования потока C++, для обслуживания и получения идей для написания структурно подобных привязок Python, ориентированных на обработку временных рядов ансамблевого прогнозирования. Одной из возможных баз кода для обработки этих данных в будущем является xframe, в настоящее время находится в предварительной версии для разработчиков. Продукты в экосистеме xtensor имеют или будут иметь привязки python, а взаимодействие C++/Python обрабатывается с помощью pybind11. Я впечатлен работой, проделанной ребятами из QuantStack, поэтому решил создать прототип привязки Python с помощью pybind11 и посмотреть, как это работает по сравнению с cffi. Кроме того, может быть проще относиться к тому, что я использую Rcpp для пакетов R. Я также не вел блог в течение многих лет, и личные заметки на github не так уж и полезны. Я благодарен другим за сообщения, помогающие преодолеть технические препятствия, и должен внести свой вклад.

План

У меня уже есть довольно хорошее представление о наборе пакетов, которые я хочу, исходя из того, где оказались пакеты R после меняющихся потребностей и этапов рефакторинга. Несмотря на то, что это поднимает планку для этапа прототипирования, у него есть преимущество в том, что он является реалистичным испытательным стендом.

Точкой входа помимо документации pybind11 является репозиторий python_example. Это единый пакет, строительные леса достаточно понятны и полезны, но на самом деле это касается только основ (как и должно быть). Моя цель здесь - несколько пакетов с правильной обработкой времени жизни объекта в этих модулях.

C API-функции

Нативные библиотеки для переноса имеют C API, но основная часть — это относительно современный C++. Правильный вопрос: зачем тогда использовать pybind11 вокруг C API, а не напрямую C++ (и это вполне может закончиться именно так). Многие причины выходят за рамки настоящего поста; достаточно сказать, что главный из них заключается в том, что C остается единственным lingua franca для двоичной, нативной совместимости. И, поскольку исходным механизмом взаимодействия является cffi, необходим C API.

C API использует непрозрачные указатели (void*), если смотреть «извне»:

void* CreateEnsembleForecastTimeSeries(date_time_to_second start, int length, const char* timeStepName);

Читателям, интересующимся практическим примером C API с непрозрачными указателями, см., например, этот заголовочный файл.

Понятно, что у pybind11 быстро возникают проблемы с такими функциями, как:

char** GetEnsembleDatasetDataIdentifiers(void* dataLibrary, int* size);

Несмотря на void* (примечание для себя: что было сообщением об ошибке?), функция с возвращаемым значением char** с указателем вывода size имеет особую семантику, которую pybind11 не может вывести. Достаточно справедливо также с учетом встроенных преобразований типов.

Таким образом, то, что может обрабатывать pybind11, должно выглядеть так:

std::vector<std::string> GetEnsembleDatasetDataIdentifiers_cpp(opaque_pointer_handle* dataLibrary)

К счастью, я могу повторно использовать некоторые утилиты генерации кода, чтобы получить большую часть шаблона.

Пакеты Python

Пакет cinterop (cinteroppyb11) отображает общие структуры C, полезные для фундаментального представления временных рядов:

typedef struct _date_time_to_second
{
    int year;
    int month;
    int day;
    int hour;
    int minute;
    int second;
} date_time_to_second;
\\\
PYBIND11_MODULE(cinteroppyb11, m) {
    py::class_<date_time_to_second>(m, “DateTimeToSecond”)
    .def(py::init<>())
    .def_readwrite(“year”, &date_time_to_second::year)
    .def_readwrite(“month”, &date_time_to_second::month)
    .def_readwrite(“day”, &date_time_to_second::day)
    .def_readwrite(“hour”, &date_time_to_second::hour)
    .def_readwrite(“minute”, &date_time_to_second::minute)
    .def_readwrite(“second”, &date_time_to_second::second);

pip install ./cinteroppyb11 работает.

И в пакете под названием uchronia:

opaque_pointer_handle* CreateEnsembleForecastTimeSeries_cpp(date_time_to_second start, int length, const std::string timeStepName){/*omitted*/}
PYBIND11_MODULE(uchronia_pb, m) {
    //
    m.def(“CreateEnsembleForecastTimeSeries”, &CreateEnsembleForecastTimeSeries_cpp, R”pbdoc( TODO doc for CreateEnsembleForecastTimeSeries)pbdoc”);
    //

pip install ./uchronia_pb работает. Но при беге

import cinteroppyb11 as c
import uchronia_pb as m
d = c.DateTimeToSecond()
d.year = 2000
# etc.
efts = m.CreateEnsembleForecastTimeSeries(d, 365, “daily”)

среда выполнения будет (может?) жаловаться на последний вызов с аргументом d. Однако на момент написания я не могу воспроизвести ошибки. Я на самом деле должен был написать этот блог по ходу дела. Я попытаюсь в другом посте вернуть все обратно и получить точное сообщение об ошибке, потому что изначально я не нашел там кристально чистого QandA. На первых шагах я столкнулся как минимум с 3 проблемами:

  • аргумент, базовый тип которого был сопоставлен в cinterop, не удалось передать функции в модуле uchronia_pb, ожидающей этот тип
  • «generic_type» «ссылка на неизвестный базовый тип»
  • и «Невозможно преобразовать возвращаемое значение функции в тип Python!», однако это ожидается для таких вещей, как opaque_pointer_handle*

Для первой проблемы я понял, что мне нужно импортировать модуль cinteroppyb11:

PYBIND11_MODULE(uchronia_pb, m) {
    py::module::import(“cinteroppyb11”);

По крайней мере, я думаю, что именно это решило первую проблему.

Наследование классов

Наследование классов хорошо заработало после нескольких раундов испытаний. Одной из практических задач было расширение структуры C multi_regular_time_series_data, которую ожидает C API, с помощью дочернего класса cpp_multi_regular_time_series_data, содержащего методы, которые затем более удобно предоставлять через pybind11:

class cpp_multi_regular_time_series_data : public /*struct*/ multi_regular_time_series_data
{
  void set_numeric_data(const std::vector<std::vector<double>>& v);
  void clear_numeric_data();
}
py::class_<multi_regular_time_series_data>(m, “MultiRegularTimeSeriesStruct”)
 .def(py::init<>())
 .def_readwrite(“ensemble_size”, &multi_regular_time_series_data::ensemble_size)
 .def_readwrite(“time_series_geometry”, &multi_regular_time_series_data::time_series_geometry)
 ;
 py::class_<cpp_multi_regular_time_series_data, multi_regular_time_series_data>(m, “MultiRegularTimeSeries”)
 .def(py::init<>())
 .def(“set_numeric_data”, &cpp_multi_regular_time_series_data::set_numeric_data)
 .def(“clear_numeric_data”, &cpp_multi_regular_time_series_data::clear_numeric_data)
 ;

Следующий

pybind11 — очень впечатляющий набор инструментов. Несмотря на то, что я провел несколько лет с изрядным количеством шаблонного программирования на C++ и взаимодействием на основе Rcpp, я по-прежнему поражен тем, на что способно шаблонное (мета) программирование.

Хотя я мог бы придерживаться cffi по разным причинам, включая наследство, я хотел бы объединить свои базы с pybind11, настроить и поделиться небольшим примером репозитория git с межмодульным совместным использованием оболочек и базовых структур C.