Реплицируйте структуру наследования в классах-оболочках Cython - PullRequest
0 голосов
/ 13 ноября 2018

Допустим, у меня определен следующий код C ++ i AB.h:

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 python от APy, чтобы мне не приходилось писать ее дважды.Я могу пропустить BPy(APy) и просто написать BPy, но тогда я должен написать

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

в определении BPy.

Я могу переименовать c_self в BPy на что-то другое (например, c_b_self), а затем назначить мой указатель на c_self и c_b_self при создании объектов BPy, но есть ли более элегантный способ достижения моей цели?

Ответы [ 2 ]

0 голосов
/ 14 ноября 2018

Удивительно, что, несмотря на естественное чувство, нет простого способа сделать 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()
 ...

Отличия от вашего примера:

  1. конструкторы имеют параметр и, следовательно, не обнуляются
  2. Я сообщаю 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), которые лучше подходят для этой работы.

0 голосов
/ 14 ноября 2018

Это не прямой ответ на ваш вопрос (было бы любопытно, если таковой есть!), Но один из вариантов - обернуть с помощью 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()])])
...