Удивительно, что, несмотря на естественное чувство, нет простого способа сделать PyB
подклассом PyA
, - ведь B
является подклассом A
!
Однако желаемая иерархия нарушает принцип подстановки Лискова некоторыми тонкими способами. Этот принцип говорит что-то вроде:
Если B
является подклассом A
, то объекты типа A
могут быть
заменены объектами типа B
без нарушения семантики
программа.
Это не совсем очевидно, потому что открытые интерфейсы PyA
и PyB
в порядке с точки зрения Лискова, но есть одно (неявное) свойство, которое делает нашу жизнь более сложной:
PyA
может обернуть любой объект типа A
PyB
может обернуть любой объект типа B
, также может сделать меньше , чем PyB
!
Это наблюдение означает, что не будет прекрасного решения проблемы, и ваше предложение использовать разные указатели не так уж и плохо.
Мое решение, представленное ниже, имеет очень похожую идею, только в том, что я скорее использую приведение (которое может немного улучшить производительность, заплатив некоторую безопасность типов), чем кэширование указателя.
Чтобы сделать пример автономным, я использую дословный код inline-C, а чтобы сделать его более общим, я использую классы без конструкторов, допускающих обнуляемые значения:
%%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
не имеет конструктора, допускающего обнуление
- Я использую raw-указатель (таким образом,
__dealloc__
требуется) для хранения ссылки, возможно, можно было бы рассмотреть возможность использования std::unique_ptr
или std::shared_ptr
, в зависимости от того, как используется класс.
- Когда создается объект класса
A
, thisptr
автоматически инициализируется на nullptr
, поэтому нет необходимости явно устанавливать thisptr
в 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
к B
(что на самом деле) вместо хранения кешированного B *
-точки.
- Существует небольшая разница между
__cinit__
и __init__
: __cinit__
родительского класса будет вызываться всегда, но __init__
родительского класса будет вызываться только тогда, когда отсутствует реализация __init__
-метод для самого класса. Таким образом, мы используем __init__
, потому что мы хотели бы переопределить / опустить установку self.thisptr
базового класса.
А теперь (он печатает в std :: out, а не в ipython-ячейке!):
>>> PyB(42).foo()
foo 42
>>> PyB(42).bar()
bar 42
Еще одна мысль: у меня был опыт, что использование наследования для «сохранения кода» часто приводило к проблемам, потому что в результате возникали «неправильные» иерархии по неправильным причинам. Могут быть и другие инструменты для сокращения стандартного кода (например, pybind11-framework, упомянутый в @chrisb), которые лучше подходят для этой работы.