pybind11: как упаковать код C++ и Python в один пакет?
Я пытаюсь собрать воедино существующий код Python и новый код C++ 11, используя CMake и pybind 11. Я думаю, что мне не хватает чего-то простого, что можно добавить в сценарии CMake, но нигде не могу его найти: примеры pybind11 содержат только код C++ и нет ни одного из Python, другие онлайн-ресурсы довольно запутаны и не актуальны - поэтому я просто не могу понять, как объединить функции на обоих языках и сделать их доступными через Python. import my_package
вниз по линии... в качестве примера, я клонировал cmake_example из pybind11 и добавил функцию mult в cmake_example/mult.py
def mult(a, b):
return a * b
как бы я сделал это видимым вместе с add
а также subtract
пройти тест ниже?
import cmake_example as m
assert m.__version__ == '0.0.1'
assert m.add(1, 2) == 3
assert m.subtract(1, 2) == -1
assert m.mult(2, 2) == 4
В настоящее время этот тест не проходит..
Спасибо!
2 ответа
Самое простое решение не имеет ничего общего с pybind11 как таковым. Что авторы обычно делают, когда хотят объединить чистый Python и C/Cython/ другие нативные расширения в одном пакете, так это следующее.
Вы создаете два модуля.
mymodule
это публичный интерфейс, чистый модуль Python_mymodule
это частная реализация, соблюдаемый модуль
Затем в mymodule
вы импортируете необходимые символы из _mymoudle
(и откат к чистой версии Python, если необходимо).
Вот пример из пакета Ярла:
try: from ._quoting import _quote, _unquote quote = _quote unquote = _unquote except ImportError: # pragma: no cover quote = _py_quote unquote = _py_unquote
Обновить
Здесь следует сценарий. Ради воспроизводимости я делаю это против оригинального cmake_example.
git clone --recursive https://github.com/pybind/cmake_example.git
# at the time of writing https://github.com/pybind/cmake_example/commit/8818f493
cd cmake_example
Теперь создайте чистые модули Python (внутри cmake_example/cmake_example
).
cmake_example/__init__.py
"""Root module of your package"""
cmake_example/math.py
def mul(a, b):
"""Pure Python-only function"""
return a * b
def add(a, b):
"""Fallback function"""
return a + b
try:
from ._math import add
except ImportError:
pass
Теперь давайте изменим существующие файлы, чтобы превратить cmake_example
модуль в cmake_example._math
,
src/main.cpp
(subtract
снято для краткости)
#include <pybind11/pybind11.h>
int add(int i, int j) {
return i + j;
}
namespace py = pybind11;
PYBIND11_MODULE(_math, m) {
m.doc() = R"pbdoc(
Pybind11 example plugin
-----------------------
.. currentmodule:: _math
.. autosummary::
:toctree: _generate
add
)pbdoc";
m.def("add", &add, R"pbdoc(
Add two numbers
Some other explanation about the add function.
)pbdoc");
#ifdef VERSION_INFO
m.attr("__version__") = VERSION_INFO;
#else
m.attr("__version__") = "dev";
#endif
}
CMakeLists.txt
cmake_minimum_required(VERSION 2.8.12)
project(cmake_example)
add_subdirectory(pybind11)
pybind11_add_module(_math src/main.cpp)
setup.py
# the above stays intact
from subprocess import CalledProcessError
kwargs = dict(
name='cmake_example',
version='0.0.1',
author='Dean Moldovan',
author_email='dean0x7d@gmail.com',
description='A test project using pybind11 and CMake',
long_description='',
ext_modules=[CMakeExtension('cmake_example._math')],
cmdclass=dict(build_ext=CMakeBuild),
zip_safe=False,
packages=['cmake_example']
)
# likely there are more exceptions, take a look at yarl example
try:
setup(**kwargs)
except CalledProcessError:
print('Failed to build extension!')
del kwargs['ext_modules']
setup(**kwargs)
Теперь мы можем построить это.
python setup.py bdist_wheel
В моем случае это производит dist/cmake_example-0.0.1-cp27-cp27mu-linux_x86_64.whl
(если компиляция C++ не удалась, это cmake_example-0.0.1-py2-none-any.whl
). Вот то, что это содержание (unzip -l ...
):
Archive: cmake_example-0.0.1-cp27-cp27mu-linux_x86_64.whl
Length Date Time Name
--------- ---------- ----- ----
0 2017-12-05 21:42 cmake_example/__init__.py
81088 2017-12-05 21:43 cmake_example/_math.so
223 2017-12-05 21:46 cmake_example/math.py
10 2017-12-05 21:48 cmake_example-0.0.1.dist-info/DESCRIPTION.rst
343 2017-12-05 21:48 cmake_example-0.0.1.dist-info/metadata.json
14 2017-12-05 21:48 cmake_example-0.0.1.dist-info/top_level.txt
105 2017-12-05 21:48 cmake_example-0.0.1.dist-info/WHEEL
226 2017-12-05 21:48 cmake_example-0.0.1.dist-info/METADATA
766 2017-12-05 21:48 cmake_example-0.0.1.dist-info/RECORD
--------- -------
82775 9 files
После того, как вы клонировали репозиторий, перейдите в каталог верхнего уровня `cmake_example'
Измените./src/main.cpp, чтобы включить функцию "mult":
#include <pybind11/pybind11.h>
int add(int i, int j) {
return i + j;
}
int mult(int i, int j) {
return i * j;
}
namespace py = pybind11;
PYBIND11_MODULE(cmake_example, m) {
m.doc() = R"pbdoc(
Pybind11 example plugin
-----------------------
.. currentmodule:: cmake_example
.. autosummary::
:toctree: _generate
add
subtract
mult
)pbdoc";
m.def("add", &add, R"pbdoc(
Add two numbers
Some other explanation about the add function.
)pbdoc");
m.def("mult", &mult, R"pbdoc(
Multiply two numbers
Some other explanation about the mult function.
)pbdoc");
(остальная часть файла такая же)
Теперь сделайте это:
$ cmake -H. -Bbuild
$ cmake --build build -- -j3
Модуль для импорта будет создан в каталоге./build. Перейдите к нему, тогда в оболочке Python ваш пример должен работать.
Для импорта пространства имен вы могли бы что-то сделать с pkgutil
:
создать структуру каталогов:
./my_mod
__init__.py
cmake_example.***.so
и еще одна параллельная структура
./extensions
/my_mod
__init__.py
cmake_example_py.py
и место в ./my_mod/__init__.py
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
from .cmake_example import add, subtract
from .cmake_example_py import mult
в ./extensions/my_mod/__init__.py
from cmake_example_py import mult
Затем добавьте оба./my_mod и./extensions/my_mod к вашему $PYTHONPATH, это может сработать (в моем примере это работает)