Cython和C++继承

13

我有两个类,A和B。B继承自A。

//C++    
class A
{
    public:
        int getA() {return this->a;};
        A() {this->a = 42;}
    private:
        int a;

};

class B: public A
{
    public:
       B() {this->b = 111;};
       int getB() {return this->b;};
    private:
        int b;

};

现在我想使用Cython对这两个类进行接口处理,并且有可能从B实例中调用getA()方法:

a = PyA()
b = PyB()
assert a.getA() == b.getA()

目前我的pyx文件看起来像这样:

cdef extern from "Inherit.h" :
    cdef cppclass A:
       int getA()

    cdef cppclass B(A):
       int getB()


cdef class PyA:
    cdef A* thisptr

    def __cinit__(self):
       print "in A: allocating thisptr"
       self.thisptr = new A()
    def __dealloc__(self):
       if self.thisptr:
           print "in A: deallocating thisptr"
           del self.thisptr

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

cdef class PyB(PyA):
    def __cinit__(self):
       if self.thisptr:
          print "in B: deallocating old A"
          del self.thisptr
       print "in B: creating new b"
       self.thisptr = new B()

    def __dealloc__(self):
       if self.thisptr:
           print "in B: deallocating thisptr"
           del self.thisptr
           self.thisptr = <A*>0

    def getB(self):
       return (<B*>self.thisptr).getB()

尽管我希望这段代码不会做出太危险的事情,但我也希望有更好的方法来处理它。

此外,使用该模块会生成以下输出:

>>> from inherit import *
>>> b = PyB()
in A: allocating thisptr
in B: deallocating old A
in B: creating new b
>>> b.getA()
42
>>> b.getB()
111
>>> del b
in B: deallocating thisptr

我并不喜欢分配一个A实例,随后立即释放它。

有没有关于如何正确地处理这个问题的建议?


这篇文章展示了一个陷阱(双重释放),而下面的答案没有注意到。 - user10661584
3个回答

8
我进行了一些实验,得出了相当成熟的答案,但现在我看到了问题所在:
如果您的扩展类型具有基本类型,则会在调用您的__cinit__方法之前自动调用基本类型的__cinit__方法;您不能显式调用继承的__cinit__方法。
因此,真正的问题是Cython类型仍然没有构造函数,只有预初始化钩子__cinit__,其行为更像默认构造函数。您无法从构造函数中调用虚拟方法,也无法从__cinit__中调用它(如果进行调用,则其行为类似于非虚拟)。
不知何故,在__cinit__内部,type(self)返回正确的类型对象,但它是无用的。Cython没有静态字段和方法,类型对象只能是type的实例(没有元类)。Python @staticmethod易于重写,因此它是无用的。
因此,除了将分配放置在def __init__(self):中并在使用它时检查已初始化的thisptr之外,没有其他方法。您可以考虑创建一个全局虚设的C++对象,并将其分配给thisptr以避免检查和崩溃。没有后初始化程序挂钩,因此您将无法检查是否已经正确初始化。

3
我以前从未见过Cython,如果我理解有误请原谅。话说回来:
在C++中,每当你有bar : foobar 继承自 foo),如果 foo 有默认构造函数,则它将在创建 bar 时自动调用……除非您为父项调用自定义构造函数。
我不了解Python,但是快速搜索告诉我相同的原则也适用于Python。即:只有当PyB没有手动调用其他构造函数时,才会调用PyA的默认构造函数。
这种情况下,是否可以这样做?
cdef class PyA:
    cdef A* thisptr

    def __cinit__(self, bypasskey="")
       if bypasskey == "somesecret"
           print "in A: bypassing allocation"
       else
           print "in A: allocating thisptr"
           self.thisptr = new A()

...

cdef class PyB(PyA):
    def __cinit__(self):
       super( PyB, self ).__init__("somesecret")

请注意,我相信这个想法很简单。但也许这里的想法会给你提供一些有用的工作思路?


这是另一个想法。我几乎可以肯定它会起作用(但语法可能有误),而且它比上面的方法要干净得多:

cdef class PyA:
    cdef A* thisptr

    def __cinit__(self, t=type(A))
           self.thisptr = new t()

...

cdef class PyB(PyA):
    def __cinit__(self):
       super( PyB, self ).__init__(type(B))

或者它会像这样吗?
cdef class PyA:
    cdef A* thisptr

    def __cinit__(self, t=A)
           self.thisptr = new t()

...

cdef class PyB(PyA):
    def __cinit__(self):
       super( PyB, self ).__init__(B)

我不是为了赏金而发布这篇文章(你也没有被强制分配给任何人),我只是想与你分享一些想法。

我认为,如果你:

a)使第二个构造函数仅对b可见(不知道是否可能),或者

b)在使用a之前检查其是否为空,或者

c)在绕过分配之前检查调用函数是否为b的构造函数,则应该能够避免“崩溃解释器”的情况。

此外,Cython C++文档明确指出,并非所有C++适配都有惯用的解决方案,其中含糊不清的引用如“可能需要其他人(包括您自己)进行一些实验,以找到处理此问题的最优雅方法。”


通过开启悬赏,我正在寻找一个在这种情况下的惯用结构。你说你不知道Python和Cython。虽然你的答案可以被改编成合法的Python(和Cython)代码,但这将给Python用户带来崩溃解释器的能力,这在我看来比浪费内存分配要糟糕得多。 - ascobol
我的回复对于评论来说太长了,请查看我帖子的后半部分。 - Mahmoud Al-Qudsi
a) 我认为在Cython中只能有一个构造函数,就像Python一样。 b) 然后在每个方法中,我们都需要检查正确的初始化... c) 也许这可以通过C模块中的任意预定义值实现。在PyA __ cinit__中,我们将检查具有此非平凡值的附加参数。在这种情况下,用户不能“意外地”使解释器崩溃。 - ascobol
@ascobol 你说得对 - 我没有考虑到 Python 可能没有多个构造函数。我已经将我的原始答案更改为只有一个构造函数(这样更安全一些/很多),并提供了一个新的答案,可能是一个惯用的解决方案。 - Mahmoud Al-Qudsi
不幸的是,c)似乎无法实现,因为PyA“构造函数”在PyB构造函数之前自动调用(请参见我的问题中的输出),因此我们无法从cython显式调用它。您的另一个想法也不能工作,原因相同,并且cinit参数必须是Python类型(纯Python类型或由Cython创建的类型),而不是C ++类型。也许Cython缺少某些功能? - ascobol

1
(我对Python和Cython都很陌生,所以请根据实际情况考虑我的回答。)如果您在__init__函数中初始化thisptr而不是__cinit__函数中,这个特定的例子似乎可以正常工作,而无需额外的分配/删除... 基本上将上面的__cinit__函数更改为:

def __init__(self):
    print "in A: creating new A"
    self.thisptr = new A()

而且

def __init__(self):
    print "in B: creating new B"
    self.thisptr = new B()

然而,我确信这至少在理论上是不安全的(也可能在实践中不安全),但也许有人可以评论一下到底有多危险...

例如,从Cython介绍paper中我们知道,“__init__不能保证被运行(例如,一个人可以创建一个子类并忘记调用祖先构造函数)。”我还没有能够构建一个测试用例来证明这种情况,但这可能是由于我对Python知识的普遍缺乏...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接