В настоящее время я рассматриваю сторонний пакет 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.