假设我在 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_self
和 c_b_self
创建 BPy
对象时,但是有没有更优雅的方式来实现我的目标?
最佳答案
令人惊讶的是,尽管感觉很自然,但没有直接的方法使 PyB
成为 PyA
的子类,毕竟 B
> 是 A
的子类!
但是,所需的层次结构违反了Liskov substitution principle以一些微妙的方式。这个原则大致说明了一些事情:
If
B
is a subclass ofA
, then the objects of typeA
can be replaced by objects of typeB
without breaking the semantics of program.
这并不是直接显而易见的,因为从 Liskov 的角度来看,PyA
和 PyB
的公共(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()
...
与您的示例的差异:
- 构造函数有一个参数,因此不能为空
- 我让 Cython 知道,
B
是A
的子类,即使用cdef 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
没有可为 null 的构造函数- 我使用原始指针(因此需要
__dealloc__
)来保存引用,也许可以考虑使用std::unique_ptr
或std::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/