Как преобразовать объект Python в std::vector типа расширения Cython и обратно?

Я использую Cython для переноса кода C++. Код содержит функцию, определенную как:

std::vector<ClassOut> analyze(std::vector<ClassIn> inputVec);

ClassIn и ClassOut являются типами расширения. Из Python я хотел бы иметь возможность вызывать эту функцию со списком или массивом numpy (все, что возможно и наиболее разумно). Я также хочу иметь доступ к типам расширений и изменять их, например:

запустить.py

from cythonCode.classIn import PyClassIn
from cythonCode.classOut import PyClassOut
from cythonCode.analyze import PyAnalyze

classIn_list = []
classIn_list.append(PyClassIn())
classIn_list.append(PyClassIn())
classOut_list = PyAnalyze(classIn_list)
print(classOut_list)

Обертки PyClassIn и PyClassOut работают нормально. Проблема заключается в простом переносе функции анализа с самого начала. Мою версию оболочки PyAnalyze можно найти ниже:

анализ.pxd

from libcpp.vector cimport vector
from classOut cimport ClassOut
from classIn cimport ClassIn, PyClassIn

cdef extern from "../cppCode/analyze.h":
  vector[ClassOut] analyze(vector[ClassIn])

анализировать.pyx

def PyAnalyze(vector<PyClassIn> inputVec)
  return analyze(inputVec)

В analysis.pyx наверняка есть ошибки. Я получаю сообщение об ошибке:

Python object type 'PyClassIn' cannot be used as a template argument

Оператор return также должен быть неверным. Cython жалуется на:

Cannot convert 'vector[ClassOut]' to Python object

У меня есть этот код в качестве минимального примера на https://github.com/zyzzler/cython-vector-minimal-example.git

EDIT: благодаря вашему вкладу я сейчас нахожусь в точке, где возвращаемый тип определения может быть обернут, но аргумент еще нет. Ссылка в первом комментарии предоставила отличную информацию о правильном типе возвращаемого значения. Итак, если я хочу обернуть функцию, определенную как:

std::vector<ClassOut> analyze(std::vector<float> inputVec);

все работает нормально! Однако мне приходится иметь дело с ClassIn типа расширения вместо float. Итак, ниже приведен код, который у меня есть сейчас:

анализировать.pyx

def PyAnalyze(classesIn):
  cdef vector[ClassOut] classesOut = analyze(classesIn)

  retval = PyClassOutVector()
  retval.move_from(move(classesOut))

  return retval

Приведенный выше код выдает ошибку:

Cannot convert Python object to 'vector[ClassIn]'

Причина этой ошибки ясна. "classesIn" - это список Python объектов PyClassIn, но для анализа (...) в качестве входных данных используется вектор [ClassIn]. Итак, вопрос в том, как преобразовать список Python в std::vector и/или из PyClassIn в ClassIn? Я пытался использовать ссылку rvalue и формализм конструктора перемещения, но это не сработало. Я также пытался сделать это с помощью такой функции:

cdef vector[ClassIn] list_to_vec(classInList):
  cdef vector[ClassIn] classInVec
  for classIn in classInList:
    classInVec.push_back(<ClassIn>classIn)
  return classInVec

Проблема здесь в операторе <ClassIn>classIn. В нем говорится:

no matching function for call to 'ClassIn::ClassIn(PyObject*&)'

Так что я действительно озадачен здесь. Как это можно решить? Я адаптировал код с минимальным примером в git, который я разместил выше.

EDIT2: чтобы предоставить дополнительную информацию для комментариев ниже. Теперь у меня есть оболочка для PyClassInVector точно такая же, как и для PyClassOutVector, см. ниже:

cdef class PyClassInVector:
  cdef vector[ClassIn] vec

  cdef move_from(self, vector[ClassIn]&& move_this):
    self.vec = move(move_this)

  def __getitem__(self, idx):
    return PyClassIn2(self, idx)

  def __len__(self):
    return self.vec.size()

cdef class PyClassIn2:
  cdef ClassIn* thisptr
  cdef PyClassInVector vector

  def __cinit__(self, PyClassInVector vec, idx):
    self.vector = vec
    self.thisptr = &vec.vec[idx]

В analyze.pxd я также добавил:

cdef extern from "<utility>":
  vector[ClassIn]&& move(vector[ClassIn]&&)

Теперь, основываясь на комментариях, в PyAnalyzefunction я бы сделал:

def PyAnalyze(classesIn):
  # classesIn is a list of PyClassIn objects and needs to be converted to a PyClassInVector
  classInVec = PyClassInVector()
  cdef vector[ClassOut] classesOut = analyze(classInVec.vec)

  retval = PyClassOutVector()
  retval.move_from(move(classesOut))

  return retval

Но, как говорится в комментарии к коду, как я могу получить список объектов PyClassIn (classesIn) в PyClassInVector (classInVec)?

EDIT3: представьте, что PyClassOut украшен атрибутом, который можно установить с помощью конструктора:

cdef class PyClassOut()
  def __cinit__(self, number):
    self.classOut_c = ClassOut(number)

  @property
  def number(self):
    return self.classOut_c.number

В run.py я делаю что-то вроде этого:

from cythonCode.classIn import PyClassIn
from cythonCode.classOut import PyClassOut
from cythonCode.analyze import PyAnalyze

classIn_list = []
classIn_list.append(PyClassIn(1))
classIn_list.append(PyClassIn(2))

classOut_list = PyAnalyze(classIn_list)

print(classOut_list[0].number)
print(classOut_list[1].number)

classOut_list по сути является retvalue из функции PyAnalyze. Возвращаемое значение — это объект PyClassOutVector. Итак, classOut_list[0] дает мне объект PyClassOut2 с индексом 0. Но здесь у меня нет доступа к атрибуту number. Также я заметил, что адрес classOut_list[1] совпадает с адресом classOut_list[0]. Я этого не понимаю. Я не совсем уверен, что делает «перемещение». Кроме того, я действительно хочу снова иметь список python как retvalue, в идеале с объектами PyClassOut вместо объектов PyClassOut2. Имеет ли это смысл? И возможно ли это?


person zyzzler    schedule 30.09.2019    source источник
comment
Что-то, основанное на этом ответе, может подойти - это позволяет вам обернуть векторы в Cython.   -  person DavidW    schedule 30.09.2019
comment
Большое спасибо за ваш вклад! Это действительно помогло исправить одну часть моей проблемы. Я сделал РЕДАКТИРОВАТЬ, объясняя это. Что касается другой части, я все еще озадачен.   -  person zyzzler    schedule 01.10.2019
comment
classIn - это объект PyClassIn - чтобы использовать вашу схему, вам нужно сообщить Cython об этом, а затем получить объект, который он обертывает: classInVec.push_back(<PyClassIn>classIn.thisptr) (или что-то подобное). Вы также можете рассмотреть возможность определения оболочки ClassInVector Python, которая позволит вам перейти к analyse без копии, если вы измените его аргумент на константную ссылку.   -  person DavidW    schedule 01.10.2019
comment
(Я немного обновил свой ответ на связанный вопрос - при повторном чтении моего примера для PyClassIn происходит ошибка, если размер вектора изменяется. Если вы не уверены, что этого не произойдет, возможно, стоит посмотреть на обновление)   -  person DavidW    schedule 01.10.2019
comment
Но можно ли применить ваш пример из связанного вопроса к PyClassIn? Я попробовал это, определив PyClassInVector с включенным move_from(...). Но должно быть наоборот по сравнению с PyClassOutVector. Я не мог понять, как это закодировать. Итак, что касается ссылки const в аргументе анализа, о котором вы упомянули, как мне это сделать?   -  person zyzzler    schedule 01.10.2019
comment
В этом случае вам действительно не нужно ничего перемещать, просто передайте classInVec.thisptr в analyze (и объявите тип classInVec как PyClassInVector)   -  person DavidW    schedule 01.10.2019
comment
Извините, я не понимаю. Мне нужно было сделать EDIT2, чтобы прояснить свою точку зрения. Надеюсь, это понятно и легко решить.   -  person zyzzler    schedule 01.10.2019
comment
Размышляя о подходе с функцией list_to_vec, мне нужно было бы расширить PyClassInVector специальным методом __setitem__. Иначе я не понимаю, как можно будет перенести список объектов PyClassIn в вектор объектов ClassIn, а вы?   -  person zyzzler    schedule 01.10.2019
comment
Я предполагал, что вы просто создадите PyClassInVector, а не list. Тем не менее, похоже, что вам может быть лучше просто работать со списком, как вы пытались сделать в редактировании 1 - этот код выглядел довольно близко к работе.   -  person DavidW    schedule 01.10.2019
comment
Функция list_to_vec теперь работает. Но у меня проблемы с PyClassOutVector. Я просмотрел изменения, которые вы внесли в свой исходный пост в связанном вопросе, чтобы посмотреть, поможет ли это. Я заметил одну вещь при реализации ваших изменений: в __cinit__ должно быть self.idx = idx вместо self.thisptr.... Тем не менее, моя проблема остается. Я описываю это в EDIT3.   -  person zyzzler    schedule 02.10.2019
comment
Если вы предпочитаете иметь список в качестве ввода и вывода, то нет причин использовать PyClassOutVector и PyClassInVector. Я также предлагаю вам забыть о move и просто копировать элементы поэлементно в PyAnalyze. Это был подход, который я предпочитаю (в основном, чтобы избежать копирования), но я думаю, что это немного сбивает с толку и не помогает вам.   -  person DavidW    schedule 02.10.2019


Ответы (1)


В комментариях я попытался порекомендовать решение, связанное с переносом векторов C++. Я предпочитаю этот подход, потому что он позволяет избежать многократного копирования памяти, но я думаю, что это вызывает больше путаницы, и вы бы предпочли просто использовать списки Python. Извиняюсь.

Чтобы использовать списки Python, вам просто нужно скопировать ввод и вывод в PyAnalyze. Вы должны сделать это вручную - автоматических преобразований не существует. Вы также должны знать о разнице между вашими обернутыми классами и базовыми классами C++. Вы можете отправлять только классы С++ на С++, а не упакованные.

Обработка ввода проста:

 def PyAnalyze(classesIn):
     # classesIn is a list of PyClassIn objects and needs to be converted to a PyClassInVector
     cdef vector[ClassIn] vecIn
     cdef vector[ClassOut] vecOut
     cdef PyClassIn a
     for a in classesIn:
         # need to type a to access its C attributes
         # Cython should check that a is of the correct type
         vecIn.push_back(a.classIn_c)

     vecOut = analyze(vecIn)

Возврат данных обратно в Cython, упакованный как PyClassOut, немного сложнее, поскольку вы не можете отправить тип C++ в конструктор Cython (все аргументы конструкторов должны быть типами Python). Просто создайте пустой PyClassOut, а затем скопируйте в него новые данные. Снова проработайте свой вектор поэлементно

 def PyAnalyze(classesIn):
     cdef PyClassOut out_val
     # ... use code above ...
     out_list = []
     for i in range(vecOut.size()):
        out_val = PyClassOut()
        out_val.classOut_c = vecOut[i]
        out_list.append(out_val)
     return out_list
person DavidW    schedule 02.10.2019
comment
Большое спасибо, это было то, что я искал! Тем не менее, способ с r-ссылкой и перемещением-конструктора может быть интересен, потому что он более производительный. Видите ли вы способ, как реализовать это в минимальном примере? - person zyzzler; 07.10.2019
comment
Ссылка проста: просто измените подпись analyze на std::vector<ClassOut> analyze(const std::vector<float>& inputVec). Это стандартный способ C++ для передачи больших объектов, которые вы не хотите изменять. Вам не нужно изменять код Cython - person DavidW; 07.10.2019
comment
Конструктор перемещения сложнее, и я, вероятно, не стал бы его делать, если вы хорошо не понимаете его сторону С++. Чтобы применить его к выходу: out_val.classOut_c = move(vecOut[i]). Вам нужно создать оператор присваивания перемещения для ClassOut и оболочку Cython для std::move (вероятно, только для ClassOut, а не функции шаблона). Я не думаю, что вы можете использовать move на входе, поскольку у вас все еще есть объекты на стороне Python/Cython. - person DavidW; 07.10.2019