如何在Python中实现一个C++类,以便被C++调用?

38 投票
6 回答
11573 浏览
提问于 2025-04-17 11:15

我有一个用C++写的类接口,还有一些用C++实现这个接口的类。这些类是在一个更大的C++程序中调用的,这个程序基本上就是“主程序”。我想用Python来写这个接口的实现,并且希望它们能在这个更大的C++程序中使用,就像它们是用C++写的一样。

关于如何让Python和C++互相配合的资料很多,但我还是没搞明白怎么实现我想要的功能。我找到的最接近的资料在这里:http://www.cs.brown.edu/~jwicks/boost/libs/python/doc/tutorial/doc/html/python/exposing.html#python.class_virtual_functions,但这并不完全符合我的需求。

为了更具体一点,假设我有一个已经定义好的C++接口,类似于:

// myif.h
class myif {
   public:
     virtual float myfunc(float a);
};

我想要实现的功能大概是这样的:

// mycl.py
... some magic python stuff ...
class MyCl(myif):
  def myfunc(a):
    return a*2

然后,在我的C++代码中,我希望能这样写:

// mymain.cc
void main(...) {
  ... some magic c++ stuff ...
  myif c = MyCl();  // get the python class
  cout << c.myfunc(5) << endl;  // should print 10
}

我希望这样说清楚了;)

6 个回答

10

引用自 http://wiki.python.org/moin/boost.python/Inheritance

“Boost.Python 还允许我们表示 C++ 的继承关系,这样被包装的派生类就可以在需要基类的值、指针或引用作为参数的地方使用。”

这里有虚函数的例子,所以解决了第一部分的问题(那个包含类 MyCl(myif) 的部分)。

关于具体的例子,可以参考 http://wiki.python.org/moin/boost.python/OverridableVirtualFunctions

对于这行代码 myif c = MyCl(); 你需要将你的 Python(模块)暴露给 C++。这里有一些例子 http://wiki.python.org/moin/boost.python/EmbeddingPython

12

这是一个简单的例子;要注意的是,因为 Base 不是纯虚拟的,所以事情变得有点复杂。我们开始吧:

  1. baz.cpp:

    #include<string>
    #include<boost/python.hpp>
    using std::string;
    namespace py=boost::python;
    
    struct Base{
      virtual string foo() const { return "Base.foo"; }
      // fooBase is non-virtual, calling it from anywhere (c++ or python)
      // will go through c++ dispatch
      string fooBase() const { return foo(); }
    };
    struct BaseWrapper: Base, py::wrapper<Base>{
      string foo() const{
        // if Base were abstract (non-instantiable in python), then
        // there would be only this->get_override("foo")() here
        //
        // if called on a class which overrides foo in python
        if(this->get_override("foo")) return this->get_override("foo")();
        // no override in python; happens if Base(Wrapper) is instantiated directly
        else return Base::foo();
      }
    };
    
    BOOST_PYTHON_MODULE(baz){
      py::class_<BaseWrapper,boost::noncopyable>("Base")
        .def("foo",&Base::foo)
        .def("fooBase",&Base::fooBase)
      ;
    }
    
  2. bar.py

    import sys
    sys.path.append('.')
    import baz
    
    class PyDerived(baz.Base):
      def foo(self): return 'PyDerived.foo'
    
    base=baz.Base()
    der=PyDerived()
    print base.foo(), base.fooBase()
    print der.foo(), der.fooBase()
    
  3. Makefile

    default:
           g++ -shared -fPIC -o baz.so baz.cpp -lboost_python `pkg-config python --cflags`
    

结果是:

Base.foo Base.foo
PyDerived.foo PyDerived.foo

在这里你可以看到 fooBase()(这个是非虚拟的 C++ 函数)是如何调用虚拟的 foo(),而这个调用会根据你使用的语言(无论是 C++ 还是 Python)来决定具体执行哪个版本。你可以在 C++ 中从 Base 类派生一个新类,它的工作方式也是一样的。

编辑(提取 C++ 对象):

PyObject* obj; // given
py::object pyObj(obj); // wrap as boost::python object (cheap)
py::extract<Base> ex(pyObj); 
if(ex.check()){ // types are compatible
  Base& b=ex(); // get the wrapped object
  // ...
} else {
  // error
}

// shorter, thrwos when conversion not possible
Base &b=py::extract<Base>(py::object(obj))();

PyObject* 构造 py::object,然后使用 py::extract 来检查你想提取的 Python 对象是否匹配:PyObject* obj; py::extract<Base> extractor(py::object(obj)); if(!extractor.check()) /* 错误 */; Base& b=extractor();

41

这个回答分为两个部分。首先,你需要在Python中暴露你的接口,这样Python的实现就可以随意覆盖其中的部分内容。接下来,你需要展示你的C++程序(在main中)如何调用Python。


将现有接口暴露给Python:

第一部分用SWIG来做其实很简单。我稍微修改了一下你的示例场景,修复了一些问题,并添加了一个额外的测试函数:

// myif.h
class myif {
   public:
     virtual float myfunc(float a) = 0;
};

inline void runCode(myif *inst) {
  std::cout << inst->myfunc(5) << std::endl;
}

现在我先不考虑在你的应用程序中嵌入Python,也就是说,你是在Python中启动异常,而不是在C++的int main()中。稍后再添加这个功能也很简单。

首先,我们要实现跨语言多态性

%module(directors="1") module

// We need to include myif.h in the SWIG generated C++ file
%{
#include <iostream>
#include "myif.h"
%}

// Enable cross-language polymorphism in the SWIG wrapper. 
// It's pretty slow so not enable by default
%feature("director") myif;

// Tell swig to wrap everything in myif.h
%include "myif.h"

为此,我们在全局范围内和特定接口上启用了SWIG的导演特性。其余的部分都是标准的SWIG用法。

我写了一个测试的Python实现:

import module

class MyCl(module.myif):
  def __init__(self):
    module.myif.__init__(self)
  def myfunc(self,a):
    return a*2.0

cl = MyCl()

print cl.myfunc(100.0)

module.runCode(cl)

有了这个,我就能够编译并运行这个:

swig -python  -c++ -Wall myif.i 
g++ -Wall -Wextra -shared -o _module.so myif_wrap.cxx -I/usr/include/python2.7 -lpython2.7

python mycl.py 
200.0
10

这正是你希望从测试中看到的结果。


在应用程序中嵌入Python:

接下来,我们需要实现一个真正的mymain.cc版本。我整理了一个可能的样子:

#include <iostream>
#include "myif.h"
#include <Python.h>

int main()
{
  Py_Initialize();

  const double input = 5.0;

  PyObject *main = PyImport_AddModule("__main__");
  PyObject *dict = PyModule_GetDict(main);
  PySys_SetPath(".");
  PyObject *module = PyImport_Import(PyString_FromString("mycl"));
  PyModule_AddObject(main, "mycl", module);

  PyObject *instance = PyRun_String("mycl.MyCl()", Py_eval_input, dict, dict);
  PyObject *result = PyObject_CallMethod(instance, "myfunc", (char *)"(O)" ,PyFloat_FromDouble(input));

  PyObject *error = PyErr_Occurred();
  if (error) {
    std::cerr << "Error occured in PyRun_String" << std::endl;
    PyErr_Print();
  }

  double ret = PyFloat_AsDouble(result);
  std::cout << ret << std::endl;

  Py_Finalize();
  return 0;
}

这基本上就是标准的在其他应用程序中嵌入Python。它可以正常工作,并且也给出了你希望看到的结果:

g++ -Wall -Wextra -I/usr/include/python2.7 main.cc -o main -lpython2.7
./main
200.0
10
10

最后一个难题是能够将你在Python中创建实例时得到的PyObject*转换为myif *。SWIG再次让这个过程变得相对简单。

首先,我们需要请求SWIG在一个头文件中为我们暴露它的运行时。我们通过额外的调用来实现这一点:

swig -Wall -c++ -python -external-runtime runtime.h

接下来,我们需要重新编译我们的SWIG模块,明确给SWIG知道的类型表命名,以便我们可以在main.cc中查找。我们使用以下命令重新编译.so文件:

g++ -DSWIG_TYPE_TABLE=myif -Wall -Wextra -shared -o _module.so myif_wrap.cxx -I/usr/include/python2.7 -lpython2.7

然后我们在main.cc中添加一个帮助函数,用于将PyObject*转换为myif*

#include "runtime.h"
// runtime.h was generated by SWIG for us with the second call we made

myif *python2interface(PyObject *obj) {
  void *argp1 = 0;
  swig_type_info * pTypeInfo = SWIG_TypeQuery("myif *");

  const int res = SWIG_ConvertPtr(obj, &argp1,pTypeInfo, 0);
  if (!SWIG_IsOK(res)) {
    abort();
  }
  return reinterpret_cast<myif*>(argp1);
}

现在这个功能已经到位,我们可以在main()中使用它:

int main()
{
  Py_Initialize();

  const double input = 5.5;

  PySys_SetPath(".");
  PyObject *module = PyImport_ImportModule("mycl");

  PyObject *cls = PyObject_GetAttrString(module, "MyCl");
  PyObject *instance = PyObject_CallFunctionObjArgs(cls, NULL);

  myif *inst = python2interface(instance);
  std::cout << inst->myfunc(input) << std::endl;

  Py_XDECREF(instance);
  Py_XDECREF(cls);

  Py_Finalize();
  return 0;
}

最后,我们必须使用-DSWIG_TYPE_TABLE=myif来编译main.cc,这样就得到了:

./main
11

撰写回答