Создаем С++ Python расширения с помощью pybind11
Когда вы хотите перенести часть функциональности в C++, то наиболее удобный способ [кмк] является использование функциональности модуля distutils который позволяет организовывать рапростроняемые библиотечки.
Первым делом создадим python-виртуальную среду (я использую virtualenvwrapper)
mkvirtualenv pybind11_test
Установка pybind11
Тут все просто, установка прямо через pip:
pip install pybind11
Создаем структуру проекта
Создадим файлы:
|- library.h
|- library.cpp
|- main.cpp
|- setup.py
- library.h и library.cpp - это файлы, в которых непосредственно будут лежать исходники нашей библиотеки.
- main.cpp - это файл, который будет инклюдить library.hpp, а также содержать код pybind11(который будет содержать meta-код для байндингов).
- setup.py - файл, который создаст распространяемую библиотеку, а также упростит процесс сборки библиотеки.
Компилируем библиотеку
Отредактируем наш setup.py
import pybind11
from distutils.core import setup, Extension
ext_modules = [
Extension(
'library', # название нашей либы
['library.cpp', 'main.cpp'], # файлики которые компилируем
include_dirs=[pybind11.get_include()], # не забываем добавить инклюды pybind11
language='c++',
extra_compile_args=['-std=c++11'], # используем с++11
),
]
setup(
name='library',
version='0.0.1',
author='user',
author_email='user@user.ru',
description='pybind11 extension',
ext_modules=ext_modules,
requires=['pybind11'] # не забываем указать зависимость от pybind11
)
Хотя у нас пока нет никакого кода, но мы уже можем собрать наше расширение, для этого запустим:
python setup.py build_ext -i
- build_ext обозначает, что мы будем собирать расширения, то есть те самые Extension, которые описаные в атрибуте ext_modules команды setup (см. setup.py).
- флаг -i значит, что мы хотим, чтобы наша библиотека (*.so файл) сгенерировалась на том же уровне, что и файл setup.py.
Если все было выполнено корректно, то рядом с setup.py появится файл вида:
library.cpython-36m-x86_64-linux-gnu.so
где library - это название библиотеки, а постфикс cpython-36m-x86_64-linux-gnu - описание конфигурации, на которой собиралось расширение.
Давайте теперь запустим python консоль
python
и выполним следующую команду:
import library
В ответ получим:
ImportError: dynamic module does not define module export function (PyInit_library)
Это значит, что библиотека была успешна считана, но необходимых функций для работы с интерпретатором в ней не нашлось. Давайте это дело поправим.
Откроем файл main.cpp и отредактируем его:
#include
PYBIND11_MODULE(library, m) {
};
#include <pybind11/pybind11.h>
- это мы подключили pybind.- PYBIND11_MODULE(library, m) - макрос, который поволяет нам определить python модуль. Важно: имя модуля (первый аргумент) должно совподать с именем, указаным в setup.py, иначе при импорте модуля у вас будет возникать ошибка.:
setup(
name='library',
# ...
)
Второй аргумент m - это, своего рода, "фабрика", с помощью которой можно расширять функциональность нашей библиотеки, добавлять функции, класы и т.д.
Итак, снова запустим сборку:
python setup.py build_ext -i
Снова откроем python консоль и выполним следующий код:
import library
print(library)
Если все верно то нас ожидает красота свидетельствующая о том, что модуль успешно подключился:
Добавляем функцию в библиотеку
Начнем с учебного примера из документации и добавим функцию, которая просто сложит два числа. Отредактируем файлы:
// file: library.cpp
#include "library.h"
int add(const int &a, const int &b) {
return a + b;
}
// file: library.h
#pragma once
int add(const int &, const int &);
// file: main.h
#include
#include "library.h" // добавили инклюд
namespace py = pybind11;
PYBIND11_MODULE(library, m) {
m.def("add", &add); // добавили функцию в модуль
};
Содержимое в library(.h|.cpp) тривиально, взглянем на главное изменение в файле main.py. Мы добавили команду:
m.def("add", &add);
- Первый аргумент - это имя функции каким оно будет доступно из python.
- Второй аргумент - это адрес функции которая будет использоваться в качестве обработчика.
Проверим что это все работает, компилируем:
python setup.py build_ext -i
Запускаем python консоль и вводим:
import library
print(library.add(40, 2))
В ответ получим:
42
Тут вас должна накрыть волна радости =)
Давайте теперь сделаем чего-нибудь интереснее.
Работаем с list
Со списками предлагается два основных способа взаимодействия.
Первый - это использование типа py::list, который, по сути, является C++ оберткой, позволяющей осуществлять прямой и последовательный доступ к элементам списка, причем тип элементов будет py::object, а, следовательно, эффективно с ними работать не получится.
Для примера добавим функцию, которая возвращает произвольный элемент. Редактируем файлы:
// file: library.cpp
#include "library.h"
#include // добавили
//...
py::object get_random(const py::list &items) {
if (items.size()) {
std::mt19937 rng;
rng.seed(std::random_device()());
std::uniform_int_distribution uint_dist(0, items.size() - 1);
return items[uint_dist(rng)];
} else {
return py::none();
};
}
// file: library.h
//...
py::object get_random(const py::list &);
// file: main.h
#include
#include "library.h"
namespace py = pybind11;
PYBIND11_MODULE(library, m) {
//...
m.def("get_random", &get_random);
};
Скомпилируем и попробуем запустить:
import library
print(library.get_random([40, 'f', {'field': 'value'}]))
# >>> f
print(library.get_random([]))
# >>> None
Как видите, наша C++ функция прекрасно воспринимает разные типы объектов; к сожалению, сам тип py::list не очень интересен и несмотря на то, что он поддерживает методы begin и end, это не позволяет применять к нему функции из stl.
Куда более широкое применение дает использование классов std::vector или std::list, в которые pybind из коробки осуществляет мэппинг. Давайте сделаем функцию, которая найдет элемент в массиве и вернет его позицию или None, если объект не найден:
// file: library.cpp
//...
py::object find_index(const std::vector &items, const py::object &value) {
auto ptr = std::find(items.begin(), items.end(), value);
if (ptr != items.end()) {
// так как мы хотим иметь возможность вернуть none,
// то приходится проводить явное преобразование типов
return py::cast(ptr - items.begin());
} else {
return py::none();
}
}
// file: library.h
//...
pybind11::object find_index(const std::vector &, const py::object &);
// file: main.h
#include
#include "library.h"
namespace py = pybind11;
PYBIND11_MODULE(library, m) {
// ...
m.def("find_index", &find_index);
};
Компилируем и запускаем:
import library
library.find_index([40, 'f', {'field': 'value'}], 'f')
# >>> 1
Надо понимать, что проверка объектов на равенство происходит по внутреннему указателю, поэтому даже если вы определили собственный объект и реализовали метод __eq__, то он использоваться не будет. Например:
import library
{'field': 'value'} == {'field': 'value'}
# >>> True
print(library.find_index([40, 'f', {'field': 'value'}], {'field': 'value'})
# >>> None
Передаем функцию в качестве аргумента
Но нам никто не мешает передать функцию в качестве параметра и использовать ее в качестве компаратора:
// file: library.cpp
#include "library.h"
#include
#include // добавили
// ...
py::object
find_index_if(
const std::vector &items,
const py::object &value,
const std::function &eq_func
) {
auto ptr = std::find_if(items.begin(), items.end(), [&value, &eq_func](const py::object &item) -> bool {
return eq_func(value, item);
});
if (ptr != items.end()) {
return py::cast(ptr - items.begin());
} else {
return py::none();
}
}
// file: library.h
#pragma once
#include
#include
#include
#include
#include // добавили
// ...
py::object find_index_if(
const std::vector &items,
const py::object &value,
const std::function &eq_func
);
// file: main.h
// ...
PYBIND11_MODULE(library, m) {
// ...
m.def("find_index_if", &find_index_if);
};
Компилируем и запускаем:
import library
library.find_index_if(
[40, 'f', {'field': 'value'}],
{'field': 'value'},
lambda x,y: x == y
)
# >>> 2
Красота.
Работаем с классами
Создавать классы с pybind тоже очень просто - для этого достаточно определить структуру (или класс). Возьмем пример из документации:
// file: library.cpp
// ...
struct Pet {
Pet(const std::string &name) : name(name) { }
void setName(const std::string &name_) { name = name_; }
const std::string &getName() const { return name; }
std::string name;
};
А дальше достаточно просто подключить в модуле:
// file: main.h
// ...
PYBIND11_MODULE(library, m) {
// ...
py::class_(m, "Pet")
.def(py::init())
.def("setName", &Pet::setName)
.def("getName", &Pet::getName);
};
Вместо использования геттера и сеттера (которые просто являются привязкой C++ функции к python-интерфейсу), можно привязать поле напрямую, используя конструкцию def_readwrite (или def_readonly для доступа только для чтения):
// file: main.h
// ...
PYBIND11_MODULE(library, m) {
// ...
py::class_(m, "Pet")
.def(py::init())
.def_readwrite("name", &Pet::name);
};
Можно еще использовать def_property (или def_property_readonly), чтобы определить доступ к полю как к свойству, то есть с неявным вызывом геттера и сеттера. Делается это так:
// file: main.h
// ...
PYBIND11_MODULE(library, m) {
// ...
py::class_(m, "Pet")
.def(py::init())
.def_property("name", &Pet::getName, &Pet::setName)
};
Переопределение методов класса
Один из важных способов работы с классами - это наличие возможности переопределять те или иные методы, причем желательно из python-кода. Понятно, что в таком случае мы будем терять в производительности, зато будем выигрывать в гибкости.
Например, пусть у нас есть класс животного, которое может издавать звук (по умолчанию животное издает пустую строку):
// file: library.h
// ...
struct Animal {
Animal() = default;;
virtual std::string get_sound() { return "";};
virtual void make_sound() { py::print(this->get_sound()); };
};
Если бы нам просто надо было пробросить класс в python, то мы бы сделали как в параграфе и написали бы:
// file: main.h
// ...
PYBIND11_MODULE(library, m) {
// ...
py::class_(m, "Animal")
.def(py::init<>())
.def("get_sound", &Animal::get_sound)
.def("make_sound", &Animal::make_sound);
};
Но проблема в том, что такой метод класса, нельзя переопределить, то есть если мы скомпилируем наш библиотеку и попытаемся вызвать следующий код:
from library import Animal
class Dog(Animal):
def get_sound(self):
return "гав-гав"
dog = Dog()
dog.make_sound()
в консоле мы увидим пустую строку:
>>>
Очевидно, это не то, что мы ожидали.
Итак, для этого надо создать класс-обертку поверх C++ класса Animal и именно эту обертку экспортировать:
// file: library.h
// ...
// класс обертка
struct PyAnimal : public Animal {
using Animal::Animal; // наследуем конструкторы
/*
* Функция обертка, необходимо создавать такую обертку для каждой функции которую планируем сделать переписываемой
*/
std::string get_sound() override {
PYBIND11_OVERLOAD(
std::string,
Animal,
get_sound
);
}
};
Также необходимо исправить код для экспортирования класса. Для этого исправим:
// file: main.h
// ...
PYBIND11_MODULE(library, m) {
// ...
py::class_(m, "Animal")
.def(py::init<>())
.def("get_sound", &Animal::get_sound)
.def("make_sound", &Animal::make_sound);
};
на:
// file: main.h
// ...
PYBIND11_MODULE(library, m) {
// ...
py::class_ animalClass(m, "Animal");
animalClass.def(py::init<>())
.def("get_sound", &Animal::get_sound)
.def("make_sound", &Animal::make_sound);
};
Компилируем и запускаем:
from library import Animal
class Dog(Animal):
def get_sound(self):
return "гав-гав"
dog = Dog()
dog.make_sound()
В консоле красота:
>>> гав-гав
Кстати, мы не стали делать обертку для функции make_sound потому, что она не используется другими функциями внутри класса и поэтому не требует дополнительных действий. Но если ее переписать, то ничего страшно не произойдет.
Работаем с dict
Работа со словарями осуществляется через мэппинг в stl класс map. Рассмотрим пример функции, которая работает по аналогии с функцией get-словаря в питоне.
Добавим следующую функцию:
// file: library.h
// ...
#include // добавили
// ...
py::object get_value(
std::map &data, // словарь
const std::string &key, // ключ, по которому вытаскиваем значения
const py::object &default_value // дефолтное значение, если ключа нет в словаре
);
И ее реализацию:
// file: library.cpp
// ...
py::object
get_value(std::map &data, const std::string &key, const py::object &default_value) {
auto it = data.find(key);
if (it == data.end()) {
return default_value;
}
return it->second;
}
Добавим мэппинг для python-класса
PYBIND11_MODULE(library, m) {
// ...
m.def(
"get_value",
&get_value,
"",
// чтобы была возможность не указывать последний аргумент
// пришлось перечислить все аргументы
py::arg("data"),
py::arg("key"),
// указываем значение по умолчанию
py::arg("default_value") = py::none()
);
};
Скомпилируем и запустим в python:
import library
library.get_value({"field1": "field1_value"}, "field1")
# >>> field1_value
library.get_value({"field1": "field1_value"}, "field2")
# >>>
library.get_value({"field1": "field1_value"}, "field2", 'default')
# >>> default
Все работает, как и ожидалось.
Естественно, надо понимать, что данную статью нельзя рассматривать как полноценный гид по работе с pybind. У данной библиотеки прекрасная документация https://pybind11.readthedocs.io/en/stable/index.html, где помимо того, что было упомянуто в данном посте, есть еще тьма полезной информации и масса хороших примеров.
На этом все, до новых встреч, друзья!