Cython包装器类中的复制继承结构

时间:2018-11-13 14:18:06

标签: inheritance cython

假设我在AB.h中定义了以下C ++代码:

class A {
public:
    void foo() {}
};

class B : public A {
public:
    void bar() {}
};

我想在Cython中包装指向这些类对象的共享指针,所以我制作了以下pxd文件:

from libcpp.memory cimport shared_ptr

cdef extern from "AB.h":
    cdef cppclass A:
        void foo()
    cdef cppclass B:
        void bar()

cdef class APy:
    cdef shared_ptr[A] c_self

cdef class BPy(APy): 
    cdef shared_ptr[B] c_self # <-- Error compiling Cython file: 'c_self' redeclared

以及以下pyx文件:

cdef class APy:
    def foo(self):
        return self.c_self.get().foo()

cdef class BPy(APy):
    def bar(self):
        return self.c_self.get().bar()

如您所见,它无法编译。我的目标是让BPy从foo继承APy python函数,这样我就不必写两次。我可以跳过BPy(APy),而只写BPy,但随后我必须写

def foo(self):
    return self.c_self.get().foo()

BPy的定义中也是如此。

我可以将c_self中的BPy重命名为其他名称(例如c_b_self),然后在创建以下对象时将我的指针分配给c_selfc_b_self BPy,但是还有实现我目标的更优雅的方式吗?

2 个答案:

答案 0 :(得分:2)

这不是您问题的直接答案(如果有一个,将很好奇!)-但一种选择是用pybind11包装-它可以轻松解决此问题。

wrapper.cpp

#include <pybind11/pybind11.h>

#include "AB.h"    

namespace py = pybind11;    
PYBIND11_MODULE(example, m) {
    py::class_<A>(m, "A")
        .def(py::init<>())
        .def("foo", &A::foo);

    py::class_<B, A>(m, "B") // second template param is parent
        .def(py::init<>())
        .def("bar", &B::bar);
}

setup.py

from setuptools import setup, Extension
import pybind11

setup(ext_modules=[Extension('example', ['wrapper.cpp'], 
                             include_dirs=[pybind11.get_include()])])

答案 1 :(得分:2)

令人惊讶的是,尽管感觉自然,但没有直接方法使PyB成为PyA的子类,毕竟B是{{1}的子类}!

但是,所需的层次结构在某些微妙的方面违反了Liskov substitution principle。这项原则大致说明了这一点:

  

如果AB的子类,则类型A的对象可以是   替换为A类型的对象,而不会破坏   程序。

这不是直接显而易见的,因为从Liskov的观点来看,BPyA的公共接口是可以的,但是有一个(隐式)属性使我们的生活更加艰难:

  • PyB可以包装PyA类型的任何对象
  • A可以包装任何类型为PyB的对象,也可以做的比<{1>}少!

此观察结果意味着将没有解决该问题的漂亮方法,并且您使用不同指针的建议也不错。

我在下面介绍的解决方案中有一个非常相似的想法,只是我使用了强制类型转换(通过支付某种类型安全性可能会稍微提高性能),而不是缓存指针。

要使示例独立,我使用内联C语言普通代码,并使其更通用,我使用没有可为空的构造函数的类:

B

与您的示例不同:

  1. 构造函数具有一个参数,因此不能为空
  2. 我让Cython知道PyB%%cython --cplus cdef extern from *: """ #include <iostream> class A { protected: int number; public: A(int n):number(n){} void foo() {std::cout<<"foo "<<number<<std::endl;} }; class B : public A { public: B(int n):A(n){} void bar() {std::cout<<"bar "<<number<<std::endl;} }; """ cdef cppclass A: A(int n) void foo() cdef cppclass B(A): # make clear to Cython, that B inherits from A! B(int n) void bar() ... 的子类,即使用B-因此我们可以省略从Acdef cppclass B(A)的转换以后。

这里是类B的包装:

A

值得注意的细节是:

  • A的类型为... cdef class PyA: cdef A* thisptr # ptr in order to allow for classes without nullable constructors cdef void init_ptr(self, A* ptr): self.thisptr=ptr def __init__(self, n): self.init_ptr(new A(n)) def __dealloc__(self): if NULL != self.thisptr: del self.thisptr def foo(self): self.thisptr.foo() ... ,而不是thisptr,因为A *没有可为空的构造函数
  • 我使用原始指针(因此需要A)来保存引用,也许可以考虑使用A__dealloc__,这取决于类的使用方式。
  • 创建类std::unique_ptr的对象时,std::shared_ptr将自动初始化为A,因此无需将thisptr显式设置为nullptrthisptr中(这就是省略nullptr的原因)。
  • 为什么使用__cinit__而不使用__cinit__很快就会变得明显。

现在是类__init__的包装器:

__cinit__

值得注意的细节:

  • B用于将... cdef class PyB(PyA): def __init__(self, n): self.init_ptr(new B(n)) cdef B* as_B(self): return <B*>(self.thisptr) # I know for sure it is of type B*! def bar(self): self.as_B().bar() 强制转换为as_B(实际上是),而不是保留缓存的thisptr指针。
  • BB *之间有细微的差别:父类的__cinit__将始终被调用,而父类的__init__将仅被调用,当类本身没有__cinit__方法的实现时。因此,我们使用__init__是因为我们想覆盖/忽略基类的__init__的设置。

现在(打印到std :: out而不是ipython-cell!):

__init__

最后一个想法:我做了一次体验,即使用继承来“保存代码”通常会导致问题,因为一个人由于错误的原因最终会出现“错误的”层次结构。可能还有其他工具可以减少样板代码(例如@chrisb提到的pybind11-framework),该工具更适合此工作。