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/ другие нативные расширения в одном пакете, так это следующее.

Вы создаете два модуля.

  1. mymodule это публичный интерфейс, чистый модуль Python
  2. _mymodule это частная реализация, соблюдаемый модуль

Затем в mymodule вы импортируете необходимые символы из _mymoudle (и откат к чистой версии Python, если необходимо).

Вот пример из пакета Ярла:

  1. quoting.py

    try:
        from ._quoting import _quote, _unquote
        quote = _quote
        unquote = _unquote
    except ImportError:  # pragma: no cover
        quote = _py_quote
        unquote = _py_unquote
    
  2. _quoting.pyx

Обновить

Здесь следует сценарий. Ради воспроизводимости я делаю это против оригинального 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, это может сработать (в моем примере это работает)

Другие вопросы по тегам