用 Cython 包装许多相似的 cpp 类

Wrap many similar cpp classes with Cython

我需要用 Cython 包装类似的 cpp classes。我可以重复多次对一个 class 有效的方法,例如:

from mycpplib import FFT2DWithFFTW1D as mycppclass

cdef class FFT2DWithFFTW1D:
    cdef mycppclass* thisptr

    def __cinit__(self, int n0=2, int n1=2):
        self.thisptr = new mycppclass(n0, n1)

    def __dealloc__(self):
        self.thisptr.destroy()
        del self.thisptr

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

    # many other functions...

为了包装其他 classes,除了第一行之外,我基本上必须编写相同的内容,并且可能会对一个或两个函数进行细微的更改。

我天真地以为我可以使用一个基数 class 并做类似

的事情
class BaseFFT2D:
    def get_local_size_X(self):
        return self.thisptr.get_local_size_X()

    # many other functions...

cdef class FFT2DWithFFTW1D(BaseFFT2D):
    cdef mycppclass* thisptr

    def __cinit__(self, int n0=2, int n1=2):
        self.thisptr = new mycppclass(n0, n1)

    def __dealloc__(self):
        self.thisptr.destroy()
        del self.thisptr

但这当然不起作用,因为 thisptr 不是 Python 对象。由于这只是一个 cpp 指针,我无法使用 Cython 关键字 public 使其可访问 Python.

有没有比为每个 cpp 重复相同代码更好的解决方案 class?

一个糟糕的解决方案是使用 Cython 关键字 include 来包含函数的定义,但它确实很丑陋。

在 Cython 中没有解决这个问题的方法(据我所知,我之前也看过一些)。但是,我认为以下(未经测试,但我认为基本正确)基于 mako 的解决方案非常干净。 Mako 是一个 python 模板引擎,通常用于生成网页,但也可用于生成 Python 或 Cython 代码。

<%!
cpp_wrappers = [('FFT2DWithFFTW1D', 'FFT2DWithFFTW1D', 'mycppclass'), 
                     ('OtherClass', 'OtherCppClass', 'OtherCppClass')]
%>

% for _, importname, cppname in cpp_wrappers:
from mycpplib import ${importname} as ${cppname}
% endfor

% for classname, _, cppname in cpp_wrappers:
cdef class ${classname}:
    cdef ${cppname}* thisptr

    def __cinit__(self, int n0=2, int n1=2):
        self.thisptr = new ${cppname}(n0, n1)

    def __dealloc__(self):
        self.thisptr.destroy()
        del self.thisptr

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

    # many other functions...

% endfor

基本上,它是您要编写的 Cython 代码的 mako 模板。正如我所写的那样,这应该在您的示例 (FFT2DWithFFTW1D) 中创建 cdef class 加上一个基于 c++ [=27] 的额外 cdef class、OtherClass =] OtherCppClass 以自己的名字导入。通过这种方式,您可以避免用一堆基本相同的 classes 写出一个长的 Cython 文件。

我喜欢让 mako 编译模板(以创建那个长的 cython 文件)作为 setup.py 的一部分。例如,我可能有一些基于以下最小脚本的东西(也未经测试,但至少给出了基本思想):

from setuptools import setup, Extension
from mako.lookup import TemplateLookup
from Cython.Distutils import build_ext
from Cython.Build import cythonize

# Set which files will be compiled
mako_files = [('modulename.mako.pyx','modulename.pyx')]
cython_files = [('modulename', 'modulename.pyx')]

# Compile mako template(s)
lookup = TemplateLookup(directories = ['.'])
for template_file, output_file in mako_files:
    template = lookup.get_template(template_file)
    with open(output_file, 'w') as outfile:
        outfile.write(template.render())

# Compile Cython files
ext_modules = []
for module_name, file_name in cython_files:
    ext_modules.append(Extension(module_name, [file_name]))
ext_modules = cythonize(ext_modules)

# Standard setup stuff
opts = dict(name='mypackage',
            requires=['cython', 'mako'],
            ext_modules=ext_modules,
            cmdclass={'build_ext': build_ext})

if __name__ == '__main__':
    setup(**opts)

Cython 已经添加了一个额外的编译阶段,在这个阶段您的 .pyx 文件被编译为 c 或 c++。该解决方案在此之前添加了另一个阶段,其中将 mako 模板编译为 Cython 文件。您可以在 mako 中使用任意 Python 代码,因此您基本上可以随心所欲地自定义不同的 classes。对于上述情况,我认为这种使用 mako 实际上可能是对充满几乎相同包装器的长文件的可读性的改进。但是,如果事情变得太复杂,显然会失去可读性。