inheritance - 在 Cython 包装类中复制继承结构

标签 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 从 APy 继承 foo python 函数,这样我就不必编写两次。我可以跳过 BPy(APy),只写 BPy,但随后我必须写

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

也在 BPy 的定义中。

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

最佳答案

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

但是,所需的层次结构违反了Liskov substitution principle以一些微妙的方式。这个原则大致说明了一些事情:

If B is a subclass of A, then the objects of type A can be replaced by objects of type B without breaking the semantics of program.

这并不是直接显而易见的,因为从 Liskov 的角度来看,PyAPyB 的公共(public)接口(interface)是可以的,但是有一个(隐式)属性使得我们的生活更艰难:

  • PyA 可以包装 A 类型的任何对象
  • PyB 可以包装 B 类型的任何对象,也可以做PyB更少的事情!

这一观察结果意味着该问题不会有完美的解决方案,并且您使用不同指针的建议并没有那么糟糕。

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

为了使示例独立,我使用内联 C 逐字代码,并使其更通用,我使用不带可为 null 构造函数的类:

%%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()
 ...

与您的示例的差异:

  1. 构造函数有一个参数,因此不能为空
  2. 我让 Cython 知道,BA 的子类,即使用 cdef cppclass B(A) - 因此我们可以稍后省略从 BA 的转换。

这是类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 没有可为 null 的构造函数
  • 我使用原始指针(因此需要__dealloc__)来保存引用,也许可以考虑使用std::unique_ptrstd::shared_ptr,取决于类的使用方式。
  • 当创建A类的对象时,thisptr会自动初始化为nullptr,因此无需显式设置 >thisptr__cinit__ 中的 nullptr(这就是省略 __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 转换为 B (实际上是),而不是保留缓存的 B *-指针。
  • __cinit____init__ 之间有一个细微的区别:父类的 __cinit__ 将始终被调用,而 __init__仅当类本身没有实现 __init__ 方法时,才会调用父类的 。因此,我们使用 __init__ ,因为我们想覆盖/省略基础类的 self.thisptr 的设置。

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

>>> PyB(42).foo()
foo 42  
>>> PyB(42).bar()
bar 42

最后一个想法:我有过这样的经验,使用继承来“节省代码”通常会导致问题,因为最终会因为错误的原因而得到“错误的”层次结构。可能还有其他工具可以减少样板代码(例如 @chrisb 提到的 pybind11-framework),它们更适合这项工作。

关于inheritance - 在 Cython 包装类中复制继承结构,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53283045/

相关文章:

java - 类型转换为更具体的类型

c++ - 抽象类作为接口(interface),没有 vtable

python - 将 Cython 与英特尔编译器和 OpenMP 结合使用

python - 使用本地安装的依赖项 pip install scikit-image

c++ - 派生到基础的转换和友元困惑

c# - 具有实现多个接口(interface)的返回类型的方法

c++ - 虚函数重载

c - 嵌入 python/cython numpy 数组 : Passing numpy array to c pointer

c++ - C/C++ 中的 Cython

python - 在 cython 中返回 c++ std::array<std::string, 4> 的包装方法