Могу ли я переопределить виртуальную функцию C++ в Python с помощью Cython?

У меня есть класс С++ с виртуальным методом:

//C++
class A
{

    public:
        A() {};
        virtual int override_me(int a) {return 2*a;};
        int calculate(int a) { return this->override_me(a) ;}

};

Что я хотел бы сделать, так это предоставить этот класс Python с помощью Cython, наследовать от этого класса в Python и иметь правильное переопределение:

#python:
class B(PyA):
   def override_me(self, a):
       return 5*a
b = B()
b.calculate(1)  # should return 5 instead of 2

Есть ли способ сделать это ? Теперь я думаю, было бы здорово, если бы мы могли переопределить виртуальный метод и в Cython (в файле pyx), но более важно позволить пользователям делать это в чистом python.

Изменить: если это поможет, решением может быть использование приведенного здесь псевдокода: http://docs.cython.org/src/userguide/pyrex_differences.html#cpdef-functions

Но тогда есть две проблемы:

  • Я не знаю, как написать этот псевдокод на Cython.
  • может есть лучший подход

person ascobol    schedule 12.04.2012    source источник
comment
Ну конечно; естественно. Он возвращает 2. Вам также нужен исходный код pyx (что совершенно неверно, но я пока не смог найти для него исправления)?   -  person ascobol    schedule 12.04.2012
comment
Нет, я не думаю, что смогу помочь. Я думаю, что boost.python поддерживает это.   -  person Sven Marnach    schedule 12.04.2012
comment
Действительно, я сделал это с boost.python много лет назад. Теперь я хотел бы попробовать альтернативы boost.python (слишком долго компилируется, результирующий модуль слишком большой, ...). Если Cython справится с этим, я думаю, все остальное пройдет гладко.   -  person ascobol    schedule 12.04.2012
comment
Я не думаю, что это поддерживается напрямую, но есть обходной путь: упоминается в списке рассылки.   -  person Fred Foo    schedule 12.04.2012
comment
Другим обходным путем может быть использование шаблона стратегии или чего-то подобного вместо перегрузки метода.   -  person katzenversteher    schedule 12.04.2012


Ответы (2)


Превосходно !

Не полный, но достаточный. Я был в состоянии сделать трюк для моей собственной цели. Объединив этот пост с источниками, указанными выше. Это было нелегко, так как я новичок в Cython, но я подтверждаю, что это единственный способ найти на сайте www.

Большое спасибо вам, ребята.

Извините, что у меня не так много времени на текстовые детали, но вот мои файлы (могут помочь получить дополнительную точку зрения на то, как собрать все это вместе)

setup.py:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

setup(
    cmdclass = {'build_ext': build_ext},
    ext_modules = [
    Extension("elps", 
              sources=["elps.pyx", "src/ITestClass.cpp"],
              libraries=["elp"],
              language="c++",
              )
    ]
)

ТестКласс:

#ifndef TESTCLASS_H_
#define TESTCLASS_H_


namespace elps {

class TestClass {

public:
    TestClass(){};
    virtual ~TestClass(){};

    int getA() { return this->a; };
    virtual int override_me() { return 2; };
    int calculate(int a) { return a * this->override_me(); }

private:
    int a;

};

} /* namespace elps */
#endif /* TESTCLASS_H_ */

ITestClass.h :

#ifndef ITESTCLASS_H_
#define ITESTCLASS_H_

// Created by Cython when providing 'public api' keywords
#include "../elps_api.h"

#include "../../inc/TestClass.h"

namespace elps {

class ITestClass : public TestClass {
public:
    PyObject *m_obj;

    ITestClass(PyObject *obj);
    virtual ~ITestClass();
    virtual int override_me();
};

} /* namespace elps */
#endif /* ITESTCLASS_H_ */

ITestClass.cpp:

#include "ITestClass.h"

namespace elps {

ITestClass::ITestClass(PyObject *obj): m_obj(obj) {
    // Provided by "elps_api.h"
    if (import_elps()) {
    } else {
        Py_XINCREF(this->m_obj);
    }
}

ITestClass::~ITestClass() {
    Py_XDECREF(this->m_obj);
}

int ITestClass::override_me()
{
    if (this->m_obj) {
        int error;
        // Call a virtual overload, if it exists
        int result = cy_call_func(this->m_obj, (char*)"override_me", &error);
        if (error)
            // Call parent method
            result = TestClass::override_me();
        return result;
    }
    // Throw error ?
    return 0;
}

} /* namespace elps */

EDIT2: примечание о виртуальных методах PURE (похоже, это довольно частая проблема). Как показано в приведенном выше коде, в этом конкретном случае "TestClass::override_me()" НЕ МОЖЕТ быть чистым, поскольку он должен вызываться в случае, если метод не переопределен в расширенном классе Python (он же: один не попадает в часть "ошибка"/"переопределение не найдено" тела "ITestClass::override_me()").

Расширение: elps.pyx:

cimport cpython.ref as cpy_ref

cdef extern from "src/ITestClass.h" namespace "elps" :
    cdef cppclass ITestClass:
        ITestClass(cpy_ref.PyObject *obj)
        int getA()
        int override_me()
        int calculate(int a)

cdef class PyTestClass:
    cdef ITestClass* thisptr

    def __cinit__(self):
       ##print "in TestClass: allocating thisptr"
       self.thisptr = new ITestClass(<cpy_ref.PyObject*>self)
    def __dealloc__(self):
       if self.thisptr:
           ##print "in TestClass: deallocating thisptr"
           del self.thisptr

    def getA(self):
       return self.thisptr.getA()

#    def override_me(self):
#        return self.thisptr.override_me()

    cpdef int calculate(self, int a):
        return self.thisptr.calculate(a) ;


cdef public api int cy_call_func(object self, char* method, int *error):
    try:
        func = getattr(self, method);
    except AttributeError:
        error[0] = 1
    else:
        error[0] = 0
        return func()

Наконец, питон вызывает:

from elps import PyTestClass as TC;

a = TC(); 
print a.calculate(1);

class B(TC):
#   pass
    def override_me(self):
        return 5

b = B()
print b.calculate(1)

Надеюсь, это должно сделать предыдущую связанную работу более прямой к тому, что мы здесь обсуждаем...

РЕДАКТИРОВАТЬ: С другой стороны, приведенный выше код можно оптимизировать, используя «hasattr» вместо блока try/catch:

cdef public api int cy_call_func_int_fast(object self, char* method, bint *error):
    if (hasattr(self, method)):
        error[0] = 0
        return getattr(self, method)();
    else:
        error[0] = 1

Приведенный выше код, конечно, имеет значение только в том случае, если мы не переопределяем метод override_me.

person Gauthier Boaglio    schedule 02.10.2012
comment
Просто примечание для тех, кто хочет попробовать этот пример: реализация TestClass() и ~TestClass() отсутствует. Это вызовет ошибку типа ImportError: ./elps.so: undefined symbol: _ZTIN4elps9TestClassE . Просто добавьте пустую встроенную реализацию - person ascobol; 27.03.2014
comment
Есть ли в вашем решении способ предоставить виртуальные методы (например, override_me() ) стороне Python? - person ascobol; 31.03.2014
comment
Пока вы меняете имя, вы должны иметь возможность: def call_override_me(self): return self.thisptr.override_me()?? - person Gauthier Boaglio; 31.03.2014
comment
Я бы предпочел оставить то же имя. Но теперь у меня есть идея: cy_call_func_int_fast может проверить, был ли переопределен override_me. Потребуется сравнить метод override_me из класса и экземпляра объекта, такого как if PyTestClass.override_me != self.__class__.override_me. Может быть, это могло бы сработать... - person ascobol; 31.03.2014
comment
Хорошо, пожалуйста, держите нас в курсе об этом. И спасибо за правку (inline constr/destr) ;) - person Gauthier Boaglio; 31.03.2014
comment
Судя по моим тестам, вроде работает. В cy_call_func вместо поиска атрибута override_me выполните проверку if self.__class__.override_me == PyA.override_me. Если true, то установите для ошибки значение 1 (мы должны использовать версию C++). Если false, используйте версию Python, так как она была перегружена. Таким образом, вы можете раскомментировать def override_me(self) в вашем файле pyx. - person ascobol; 01.04.2014
comment
У меня есть реализация здесь, которую я передаю args и kwargs, чтобы быть более общей функцией. Чтобы на самом деле создать args и kwargs, можно использовать Py_BuildValue для простых конструкций или прибегнуть к другому public API, чтобы Cython выполнял тяжелую работу. - person dashesy; 05.10.2015

Решение несколько сложное, но возможное. Здесь есть полностью рабочий пример: https://bitbucket.org/chadrik/cy-cxxfwk/overview

Вот обзор техники:

Создайте специализированный подкласс class A, целью которого будет взаимодействие с расширением cython:

// created by cython when providing 'public api' keywords:
#include "mycymodule_api.h"

class CyABase : public A
{
public:
  PyObject *m_obj;

  CyABase(PyObject *obj);
  virtual ~CyABase();
  virtual int override_me(int a);
};

Конструктор принимает объект python, который является экземпляром нашего расширения cython:

CyABase::CyABase(PyObject *obj) :
  m_obj(obj)
{
  // provided by "mycymodule_api.h"
  if (import_mycymodule()) {
  } else {
    Py_XINCREF(this->m_obj);
  }
}

CyABase::~CyABase()
{
  Py_XDECREF(this->m_obj);
}

Создайте расширение этого подкласса в cython, реализовав все невиртуальные методы стандартным образом.

cdef class A:
    cdef CyABase* thisptr
    def __init__(self):
        self.thisptr = new CyABase(
            <cpy_ref.PyObject*>self)

    #------- non-virutal methods --------
    def calculate(self):
        return self.thisptr.calculate()

Создайте виртуальные и чисто виртуальные методы как public api функции, которые принимают в качестве аргументов экземпляр расширения, аргументы метода и указатель ошибки:

cdef public api int cy_call_override_me(object self, int a, int *error):
    try:
        func = self.override_me
    except AttributeError:
        error[0] = 1
        # not sure what to do about return value here...
    else:
        error[0] = 0
        return func(a)

Используйте эти функции в своем промежуточном звене С++ следующим образом:

int
CyABase::override_me(int a)
{
  if (this->m_obj) {
    int error;
    // call a virtual overload, if it exists
    int result = cy_call_override_me(this->m_obj, a, &error);
    if (error)
      // call parent method
      result = A::override_me(i);
    return result;
  }
  // throw error?
  return 0;
}

Я быстро адаптировал свой код под ваш пример, так что могли быть ошибки. Взгляните на полный пример в репозитории, и он должен ответить на большинство ваших вопросов. Не стесняйтесь разветвлять его и добавлять свои собственные эксперименты, это далеко не завершено!

person chadrik    schedule 21.07.2012
comment
Это отличное начало, большое спасибо. Но может ли метод override_me() быть вызван сценарием Python? Если этот метод не является чисто виртуальным в С++, тогда его можно будет вызвать из части Python. - person ascobol; 31.03.2014
comment
Я составил руководство monadical.com/posts/virtual-classes-in -cython.html - person jdcaballerov; 03.02.2021