【问题标题】:Can I override a C++ virtual function within Python with Cython?我可以使用 Cython 在 Python 中覆盖 C++ 虚函数吗?
【发布时间】:2012-04-12 15:22:23
【问题描述】:

我有一个带有虚方法的 C++ 类:

//C++
class A
{

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

};

我想做的是用 Cython 将这个类公开给 Python,从 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 中编写此伪代码
  • 也许有更好的方法

【问题讨论】:

  • 当然可以。它返回 2。你还需要 pyx 源吗(这是完全错误的,但我还没有找到修复它的方法)?
  • 不,我想我帮不上忙。我认为 boost.python 支持这一点。
  • 确实,我在几年前就使用 boost.python 做到了。现在我想尝试 boost.python 的替代方案(编译时间太长,生成的模块太大,...)。如果 Cython 能处理好这件事,我认为其余的会顺利进行。
  • 我不认为这是直接支持的,但解决方法是mentioned on the mailing list
  • 另一种解决方法是使用策略模式或类似的东西,而不是方法重载。

标签: c++ python cython


【解决方案1】:

解决方案有些复杂,但有可能。这里有一个完整的示例: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)

像这样在你的 c++ 中间体中使用这些函数:

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;
}

我很快根据您的示例调整了我的代码,因此可能会出现错误。查看存储库中的完整示例,它应该可以回答您的大部分问题。随意分叉并添加您自己的实验,它远未完成!

【讨论】:

  • 这是一个很好的开始,非常感谢。但是 Python 脚本可以调用 override_me() 方法吗?如果此方法在 C++ 中不是纯虚拟方法,那么应该可以从 Python 部分调用它
【解决方案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()

最后,python 调用:

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' 方法的情况下才会有所不同。

【讨论】:

  • 对于那些想尝试这个例子的人来说,请注意:缺少 TestClass() 和 ~TestClass() 的实现。这将导致类似“ImportError: ./elps.so: undefined symbol: _ZTIN4elps9TestClassE”的错误。只需添加一个空的内联实现
  • 您的解决方案是否有一种方法可以将虚拟方法(即 override_me() )暴露给 Python 端?
  • 只要改名应该可以:def call_override_me(self): return self.thisptr.override_me()??
  • 我希望保持相同的名称。但我现在有了一个想法:cy_call_func_int_fast 可以检查 override_me 是否已被覆盖。它需要比较类中的方法 override_me 和实例化对象(如if PyTestClass.override_me != self.__class__.override_me)。也许它可以工作......
  • 好的,请随时通知我们。并感谢您的编辑 (inline constr/destr) ;)
猜你喜欢
  • 1970-01-01
  • 2016-02-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-16
  • 1970-01-01
  • 2015-06-17
  • 2010-10-14
相关资源
最近更新 更多