В чем разница между различными вариантами преобразования типа pybind11?

У меня есть проект, в котором я смешиваю код cpp и python.

По нескольким причинам интерфейс должен быть на python, а сервер на cpp.

теперь я ищу решение о том, как передать мой объект python в cpp. следует отметить тот факт, что бэкенду в какой-то момент необходимо выполнить обратный вызов в python для вычисления некоторых чисел, где функция python вернет список с плавающей запятой.

Я просматривал параметры преобразования типа pybind, определенные здесь: https://pybind11.readthedocs.io/en/stable/advanced/cast/index.html

Однако мне кажется, что вариант 1 довольно прост в использовании, как я вижу здесь: https://pybind11.readthedocs.io/en/stable/advanced/classes.html#overriding-virtual-functions-in-python

поэтому мне интересно, почему кто-то выбрал номер 3? чем он отличается от варианта 1?

Большое спасибо


person katetsu    schedule 01.02.2020    source источник
comment
Следующая ссылка может приятно дополнить ваше чтение документации: github.com/pybind/pybind11/issues/ 1201   -  person Pranav Vempati    schedule 02.02.2020


Ответы (1)


Да, если основной код написан на C++ и привязки хорошо детализированы, то с вариантом 1 проще всего работать, так как в этом случае связанные объекты C++ так же естественно использовать в Python, как и собственные классы Python. Это упрощает жизнь, потому что вы получаете полный контроль над идентификацией объекта и над тем, копировать его или нет.

Для 3 я считаю, что pybind11 слишком агрессивен с копированием при использовании обратных вызовов (как кажется, ваш вариант использования), например. с массивами numpy вполне возможно работать с буфером на стороне С++, если он проверен на непрерывность. Конечно, копирование защитит от проблем с памятью, но слишком мало контроля над копированием по сравнению с обычным. без копирования (у numpy та же проблема, что и у tbs).

Причина, по которой существует 3, в основном состоит в том, что он улучшает удобство использования и обеспечивает приятный синтаксис. Например, если у нас есть функция с такой сигнатурой:

void func(const std::vector<int>&)

тогда хорошо иметь возможность вызывать его со стороны Python как func((1, 2, 3)) или даже func(range(3)). Он удобен, прост в использовании, выглядит аккуратно и т. д. Но в этом случае нет другого выхода, кроме как копировать, так как структура памяти tuple сильно отличается от std::vector (и диапазон даже не представляет контейнер памяти).

Обратите внимание, однако, что в приведенном выше примере func вызывающая сторона все еще может решить предоставить связанный объект std::vector<int> и, таким образом, предотвратить любое копирование. Может выглядеть не так красиво, но есть полный контроль. Это полезно, например, если вектор является возвращаемым значением какой-то другой функции или изменяется между вызовами:

v = some_calc()   # with v a bound C++ vector
func(v)
v.append(4)       # add an element
func(v)

Сравните это со случаем, когда список поплавков возвращается после вычисления некоторых чисел, аналогично (но не совсем) вашему описанию:

std::list<float> calc()

Если вы выберете «вариант 1», то связанная функция calc вернет связанный объект C++ std::list<float>. Если вы выберете «вариант 3», то связанная функция calc вернет Python list с скопированным в него содержимым C++ std::list<float>.

Проблема, которая возникает с «вариантом 3», заключается в том, что если вызывающему объекту действительно нужен связанный объект C++, тогда значения необходимо скопировать обратно в новый список, поэтому всего 2 копии. OTOH, если вы выберете «вариант 1», а вызывающему абоненту вместо этого нужен Python list, то он может свободно копировать возвращаемое значение calc, если это необходимо:

res = calc()
list_res = list(res)

или даже, если они хотят этого все время:

def pycalc():
    return list(calc())

Теперь, наконец, к вашему конкретному случаю, когда это обратный вызов Python, вызванный из C++, который возвращает список с плавающей запятой. Если вы используете «вариант 1», то функция Python вынуждена создать список C++ для возврата, например (с типом cpplist имя, присвоенное связанному типу std::list<float>):

def pycalc():
    return cpplist(range(3))

который программист Python не нашел бы красивым. Вместо этого, выбрав «вариант 3», проверив тип возвращаемого значения и при необходимости выполнив преобразование, это также будет допустимо:

def pycalc():
    return [x for x in range(3)]

В зависимости от общих требований и типичных вариантов использования «вариант 3» может быть более ценным для ваших пользователей.

person Wim Lavrijsen    schedule 02.02.2020
comment
Спасибо всем за информацию. Чтобы понять это немного лучше, правильно ли сказать, что при использовании варианта 1 pybind генерирует для меня типы Python, и эти типы размещаются в памяти по-другому по сравнению с их версиями C++? Учитывая первое, не копирует ли вариант 1 данные при передаче типов в С++? (неявно). Наконец, следует ли ожидать увидеть какую-либо разницу в производительности между вариантом 1 и вариантом 3 при выполнении кода на стороне C++? - person katetsu; 02.02.2020
comment
Вариант 1 генерирует прокси-серверы Python, которые упаковывают объекты C++ на место, т.е. они содержат указатели на базовые объекты C++ и пересылают все (вызовы и доступ к данным), поэтому никаких копий, даже неявно. Для производительности очень важно, что вы делаете. Например. итерация по связанному std::vector по прокси-серверу медленнее (так не должно быть, но pybind11 имеет ужасную производительность), чем итерация по кортежу, но копирование 1M элементов только для доступа к нескольким выбранным элементам плохо наоборот. Итак, это снова сводится к вашим конкретным вариантам использования. - person Wim Lavrijsen; 02.02.2020