Да, если основной код написан на 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