Python 與 C++ 的互動編程
最後更新:2018-07-31
來源:互聯網
上載者:User
基礎知識
編譯語言和解釋語言
儘管現在很多編程技術都在交融,出現了不少帶有編譯系統的解釋語言,和帶有複雜運行時系統的編譯語言,但通常我們還是可以比較明確的區分它們。在這個大前提下,Python和C++屬於這個兩類技術中比較極端的代表。
編譯時間和運行時
C++的強大之處在於高效的產生產品和強大複雜的編譯系統。利用C++的文法,可以產生非常靈活強大的程式,而這其中大部分工作都是在編譯器的過程中完成。相對而言,C++沒有標準的運行時(CLI系統還沒有真正成為C++的標準,只能說提供了一個C++在CLI系統上啟動並執行標準),要建立一個完善的運行時,要麼就要投入大量的成本,要麼就尋找一個合適的運行時環境調用。Python自身是一個成熟完備的指令碼語言,擁有開放的編程和擴充介面,加上它本身的對象機制設計非常適合外部調用,Python與編譯語言,特別是C/C++的互嵌入日益成為關注焦點。
互嵌入
Python有多種實現,比較著名的有C語言編寫的官方實現,Java版的Jython,.net版的IronPython,值得一提的是,後兩者都是由 Jim Hugunin 開發,此人現在微軟主持動態語言研究項目。另外,有一個CPython調用.net的橋接系統 PythonDotNet,由ZOPE社區的一位技術人員開發。現在使用最廣泛的互嵌入場合,是C/C++和CPython之間的互嵌入。
Python 的官方版本本身就是由C語言編寫,有開放的C介面。使用C語言調用和擴充Python都非常簡單。基於這個前提,使用C++語言與Python進行互操作也並不複雜。現在已經有若干用於Python API封裝的C++庫,這裡我們要討論的是Boost.Python。這是著名的C++庫Boost的重要組成之一。
用C++擴充Python
Python擴充入門
Python允許在遵循一定規則的前提下調用語言和作業系統無關的動態連結程式庫。通常,在Windows下為.dll,而在*nix下為.so檔案。作業系統的區別由C++編譯器、Boost庫和Python解譯器屏蔽,最終程式員可以基本忽視其中的區別。
在實現互嵌入的時候,要考慮Python的運行時限制,Python的虛擬機器只能操作Python對象,並且虛擬機器擁有自己的記憶體管理員模式,應該根據情況對C++程式和Python環境所交換的資訊進行必要的封裝和拆封。
Python本身提供了C語言擴充所需要的介面檔案,可以在Python的include,Libs,DLLs目錄下找到對應的檔案。
一個純C語言的Python擴充可能類似下面這樣:
#include <Python.h>
static PyObject *
spam_system(PyObject *self, PyObject *args)
{
const char *command;
int sts;
if (!PyArg_ParseTuple(args, "s", &command))
return NULL;
sts = system(command);
return Py_BuildValue("i", sts);
}
我們根據具體的作業系統和開發工具,把以上函數封裝到一個動態連結程式庫中,放入Python的動態連結程式庫搜尋路徑(Python For Windows中是DLLs)中。就可以把它作為Python的一個模組使用。
C++封裝
我們知道,C++的代碼封裝機制和C有所不同,相比真正的純C語言,C++更為複雜。例如,它有虛函數,有模版,前者使運行時更為複雜和靈活,後者使一些比宏更複雜的文法推導得以實現。Boost.Python為C++程式提供的封裝能力,正是基於template。我們要做的,是把C++程式中的函數、類、資料成員等等,都變成Python對象。
BOOST.Python封裝了各平台的Python動態連結程式庫介面,我們要做的只是調用它,封裝需要暴露的定義,然後指示編譯器產生動態連結程式庫即可。
函數封裝
Python的API使用回呼函數調用Python對象和函數。BOOST提供完整的函數封裝過程,一個簡單的封裝過程如下:
Hello World
Following C/C++ tradition, let's start with the "hello, world". A C++ Function:
char const* greet()
{
return "hello, world";
}
can be exposed to Python by writing a Boost.Python wrapper:
#include <boost/python.hpp>
using namespace boost::python;
BOOST_PYTHON_MODULE(hello)
{
def("greet", greet);
}
That's it. We're done. We can now build this as a shared library. The resulting DLL is now visible to Python. Here's a sample Python session:
>>> import hello
>>> print hello.greet()
hello, world
以上來自BOOST文檔。
物件導向
對象管理、引用計數
Python虛擬機器提供引用計數和自動記憶體回收能力,但是C++對象沒有這樣的能力(C++對象運行在Python虛擬機器之外)。為了避免程式運行過程中的記憶體使用量問題,需要為函數中傳遞的指標提供引用計數管理,BOOST通過“調用協議”來支援此功能,詳情請見BOOST文檔“Call Policies”。
C++ 對象到 Python 對象的封裝
BOOST文檔中提供了C++對象封裝至Python對象的方法,主要手段是以class_模板將C++類定義解析為符合C API的形式,一個形如:
struct World
{
void set(std::string msg) { this->msg = msg; }
std::string greet() { return msg; }
std::string msg;
};
的C++代碼可以通過如下方式封裝:
#include <boost/python.hpp>
using namespace boost::python;
BOOST_PYTHON_MODULE(hello)
{
class_<World>("World")
.def("greet", &World::greet)
.def("set", &World::set)
;
}
這裡的關鍵點在於封裝代碼必有的 BOOST_PYTHON_MODULE 宏和class<>模板的def。為了支援C++類定義豐富的內容,Boost.Python提供了很多調用方法,比較常用,也比較重要的是建構函式、重載和虛函數的支援。
建構函式的封裝是通過__init__模板封裝的,不同的建構函式重載可以通過參數列表區分並重複封裝。BOOST文檔中給出了這樣一個樣本:
class_<World>("World", init<std::string>())
.def(init<double, double>())
.def("greet", &World::greet)
.def("set", &World::set)
;
普通函數重載的支援事實上是一種函數封裝方法,和這個函數是否是某個類的成員並無關係。基本思路是把不同的函數形式定義為不同的函數指標,然後以普通函數的形式封裝。
虛函數的支援比較複雜,BOOST通過多繼承方式支援它,使用者需要以多繼承定義一個基類的封裝,然後暴露這個封裝類。在閱讀BOOST文檔時,要注意封裝類在用class_暴露介面時的代碼,以下是BOOST文檔中的一個樣本:
struct BaseWrap : Base, wrapper<Base>
{
int f()
{
if (override f = this->get_override("f"))
return f(); // *note*
return Base::f();
}
int default_f() { return this->Base::f(); }
};
class_<BaseWrap, boost::noncopyable>("Base")
.def("f", &Base::f, &BaseWrap::default_f)
;
注意封裝代碼中class_的模板參數是BaseWrap,但def中的第二個函數是&Base::f。def的第三個函數提供函數封裝的一些資訊,這裡指明了該虛函數的預設實現。更多的用法請參考Boost文檔。在BOOST中還提供了更複雜的功能,比如可以定義Python類的屬性(者甚至比在Python中更簡單)。
在C++程式中嵌入Python
引用解譯器
Python標準的外部調用方法並不複雜,在Python的文檔中可以查到。最簡單的調用方式如下:
#include <Python.h>
int
main(int argc, char *argv[])
{
Py_Initialize();
PyRun_SimpleString("from time import time,ctime\n"
"print 'Today is',ctime(time())\n");
Py_Finalize();
return 0;
}
Py_Initialize();啟動虛擬機器,Py_Finalize();終止它。PyRun_SimpleString用於執行語句。Boost延續了這一方式,並著眼於將互動過程變得更簡單些。BOOST程式中最簡單的樣本如下:
Py_Initialize();
object main_module((
handle<>(borrowed(PyImport_AddModule("__main__")))));
object main_namespace = main_module.attr("__dict__");
handle<> ignored((PyRun_String(
"hello = file('hello.txt', 'w')\n"
"hello.write('Hello world!')\n"
"hello.close()"
, Py_file_input
, main_namespace.ptr()
, main_namespace.ptr())
));
Py_Finalize();
從這種複雜度的程式中,體現不出BOOST的優勢,它甚至比C程式更複雜。不過這裡可以學習一下它的基本架構。也許下面這個更為複雜的樣本可以說明些問題:
object main_module((
handle<>(borrowed(PyImport_AddModule("__main__")))));
object main_namespace = main_module.attr("__dict__");
handle<> ignored((PyRun_String(
"result = 5 ** 2"
, Py_file_input
, main_namespace.ptr()
, main_namespace.ptr())
));
int five_squared = extract<int>(main_namespace["result"]);
我們可以通過這裡看到BOOST的一些優勢,如通過C++對象(這裡main_namespace對象)擷取Python資料,並將其轉化為C++資料(extract模板)。而下面這個樣本示範了BOOST為Python環境提供的C++異常保護:
try
{
object result((handle<>(PyRun_String(
"5/0"
, Py_eval_input
, main_namespace.ptr()
, main_namespace.ptr()))
));
// execution will never get here:
int five_divided_by_zero = extract<int>(result);
}
catch(error_already_set)
{
// handle the exception in some way
}
當然,我們也可以選擇無異常的版本:
handle<> result((allow_null(PyRun_String(
"5/0"
, Py_eval_input
, main_namespace.ptr()
, main_namespace.ptr()))));
if (!result)
// Python exception occurred
else
// everything went okay, it's safe to use the result
呼叫指令碼
PyRunString是Python的C-API,Python提供了一系列的調用方法供程式執行指令碼使用,也包括了PyRun_File這樣的直接調用檔案的功能,當然,它不支援C++的I/O,使用的是C風格的檔案指標。
附記
最後提一下,使用Boost,無論你是要擴充還是嵌入Python,都需要先行編譯Boost庫,編譯方法在Boost的文檔中也有介紹。使用Boost的時候,要把先行編譯好的lib連結到link序列中,還要把DLL放到應用程式可以訪問到的地方。