1.在其它 App 內嵌入 Python¶
前面的章節討論了如何擴充 Python,也就是如何透過附加一個 C 函式庫來擴充 Python 的功能。但也可以反過來做:將 Python 嵌入你的 C/C++ 應用程式中。嵌入讓你的應用程式能夠以 Python 而非 C 或 C++ 來實作應用程式的某些功能。這可以用於許多目的;其中一個例子是允許使用者透過撰寫一些 Python 腳本來根據他們的需求客製化應用程式。如果某些功能用 Python 寫起來比較容易,你也可以自己使用這種方法。
嵌入 Python 與擴充 Python 類似,但不完全相同。差別在於當你擴充 Python 時,應用程式的主程式仍然是 Python 直譯器,而當你嵌入 Python,主程式可能與 Python 無關 — 相反地,應用程式的某些部分偶爾會呼叫 Python 直譯器來執行一些 Python 程式碼。
所以如果你要嵌入 Python,你要提供自己的主程式。這個主程式必須做的事情之一是初始化 Python 直譯器,或至少必須要呼叫函式Py_Initialize()。還有一些可選的呼叫來傳遞命令列引數給 Python。然後你就可以在應用程式的任何部分呼叫直譯器。
有幾種不同的方式來呼叫直譯器:你可以傳遞一個包含 Python 陳述式的字串給PyRun_SimpleString(),或者你可以傳遞一個 stdio 檔案指標和檔案名稱(僅用於錯誤訊息中的識別)給PyRun_SimpleFile()。你也可以呼叫前面章節中描述的較低層級操作來建構和使用 Python 物件。
也參考
- Python/C API 參考手冊
Python 的 C 介面詳細資訊在此手冊中提供。大量必要的資訊可以在這裡找到。
1.1.非常高階的嵌入¶
嵌入 Python 最簡單的形式是使用非常高階的介面。此介面用於執行 Python 腳本而無需直接與應用程式互動。例如這可以用來對檔案執行一些操作。
#define PY_SSIZE_T_CLEAN#include<Python.h>intmain(intargc,char*argv[]){PyStatusstatus;PyConfigconfig;PyConfig_InitPythonConfig(&config);/* 建議但非必要 */status=PyConfig_SetBytesString(&config,&config.program_name,argv[0]);if(PyStatus_Exception(status)){gotoexception;}status=Py_InitializeFromConfig(&config);if(PyStatus_Exception(status)){gotoexception;}PyConfig_Clear(&config);PyRun_SimpleString("from time import time,ctime\n""print('Today is', ctime(time()))\n");if(Py_FinalizeEx()<0){exit(120);}return0;exception:PyConfig_Clear(&config);Py_ExitStatusException(status);}
備註
#definePY_SSIZE_T_CLEAN 被用來指示某些 API 應該使用Py_ssize_t 而不是int。從 Python 3.13 開始不再需要它,但我們保留它以維持向後相容性。關於此巨集的描述請參閱字串與緩衝區。
PyConfig.program_name 的設定應該在Py_InitializeFromConfig() 前呼叫,以告知直譯器 Python run-time 函式庫的路徑。接下來,Python 直譯器會用Py_Initialize() 初始化,然後執行一個硬編碼的 Python 腳本來印出日期和時間。之後,Py_FinalizeEx() 呼叫會關閉直譯器,接著程式結束。在真實的程式中,你可能想要從另一個來源取得 Python 腳本,或許是文字編輯器例程、檔案或資料庫。從檔案取得 Python 程式碼可以更好地使用PyRun_SimpleFile() 函式來完成,這樣可以省去分配記憶體空間和載入檔案內容的麻煩。
1.2.超越非常高階嵌入:概觀¶
高階介面讓你能夠從應用程式中執行任意的 Python 程式碼片段,但交換資料值的過程可以說相當繁瑣。如果你想進行這類操作,應該使用較低階的呼叫。雖然需要撰寫更多的 C 程式碼,但幾乎可以實現任何功能。
需要注意的是,雖然目的不同,但擴充 Python 與嵌入 Python 其實是非常相似的操作,前面章節討論的大多數主題在這裡同樣適用。為了說明這一點,請思考從 Python 到 C 的擴充程式碼實際上做了什麼:
將資料值從 Python 轉換為 C,
使用轉換後的值呼叫 C 例程,並
將呼叫中的資料值從 C 轉換為 Python。
當嵌入 Python 時,介面程式碼會:
將資料值從 C 轉換為 Python,
使用轉換後的值呼叫 Python 介面例程,並
將呼叫中的資料值從 Python 轉換為 C。
如你所見,資料轉換的步驟只是互換了順序,以配合跨語言傳遞方向的不同。唯一的差別在於兩個資料轉換之間所呼叫的例程:在擴充時你呼叫的是 C 例程;在嵌入時則呼叫 Python 例程。
本章不會討論如何將資料從 Python 轉換為 C 或從 C 轉換回 Python,且假設讀者已經知道參照的正確使用方式與錯誤處理。由於這些部分與擴充直譯器時相同,相關資訊可參考前面的章節。
1.3.純嵌入¶
第一個程式的目標是執行 Python 腳本中的函式。就像在非常高階介面的章節中一樣,Python 直譯器不會直接與應用程式互動(但這在下一節會改變)。
執行 Python 腳本中定義函式的程式碼是:
#define PY_SSIZE_T_CLEAN#include<Python.h>intmain(intargc,char*argv[]){PyObject*pName,*pModule,*pFunc;PyObject*pArgs,*pValue;inti;if(argc<3){fprintf(stderr,"Usage: call pythonfile funcname [args]\n");return1;}Py_Initialize();pName=PyUnicode_DecodeFSDefault(argv[1]);/* Error checking of pName left out */pModule=PyImport_Import(pName);Py_DECREF(pName);if(pModule!=NULL){pFunc=PyObject_GetAttrString(pModule,argv[2]);/* pFunc is a new reference */if(pFunc&&PyCallable_Check(pFunc)){pArgs=PyTuple_New(argc-3);for(i=0;i<argc-3;++i){pValue=PyLong_FromLong(atoi(argv[i+3]));if(!pValue){Py_DECREF(pArgs);Py_DECREF(pModule);fprintf(stderr,"Cannot convert argument\n");return1;}/* pValue reference stolen here: */PyTuple_SetItem(pArgs,i,pValue);}pValue=PyObject_CallObject(pFunc,pArgs);Py_DECREF(pArgs);if(pValue!=NULL){printf("Result of call: %ld\n",PyLong_AsLong(pValue));Py_DECREF(pValue);}else{Py_DECREF(pFunc);Py_DECREF(pModule);PyErr_Print();fprintf(stderr,"Call failed\n");return1;}}else{if(PyErr_Occurred())PyErr_Print();fprintf(stderr,"Cannot find function\"%s\"\n",argv[2]);}Py_XDECREF(pFunc);Py_DECREF(pModule);}else{PyErr_Print();fprintf(stderr,"Failed to load\"%s\"\n",argv[1]);return1;}if(Py_FinalizeEx()<0){return120;}return0;}
此程式碼使用argv[1] 載入 Python 腳本,並呼叫argv[2] 中所指定的函式。其整數引數則來自argv 陣列中的其他值。如果你編譯並連結此程式(我們稱完成的可執行檔為call),並用它來執行 Python 腳本,例如:
defmultiply(a,b):print("Will compute",a,"times",b)c=0foriinrange(0,a):c=c+breturnc
那麼結果應該是:
$callmultiplymultiply32Will compute 3 times 2Result of call: 6
雖然以其功能而言這個程式相當龐大,但大部分的程式碼是用於 Python 與 C 之間的資料轉換以及錯誤回報。至於與嵌入 Python 有關的重點部分,則從以下開始:
Py_Initialize();pName=PyUnicode_DecodeFSDefault(argv[1]);/* Error checking of pName left out */pModule=PyImport_Import(pName);
在初始化直譯器後,腳本使用PyImport_Import() 載入。此例程需要一個 Python 字串作為其引數,該字串使用PyUnicode_DecodeFSDefault() 資料轉換例程建構。
pFunc=PyObject_GetAttrString(pModule,argv[2]);/* pFunc is a new reference */if(pFunc&&PyCallable_Check(pFunc)){...}Py_XDECREF(pFunc);
腳本載入後,會使用PyObject_GetAttrString() 來取得所需的名稱。如果該名稱存在,且回傳的物件是可呼叫的,便可安全地假設它是一個函式。接著,程式會以一般方式建立引數的元組。之後便以下列方式呼叫該 Python 函式:
pValue=PyObject_CallObject(pFunc,pArgs);
函式回傳時,pValue 要不是NULL 就是包含函式回傳值的參照。請務必在檢查值之後釋放參照。
1.4.擴充嵌入式 Python¶
到目前為止,嵌入式 Python 直譯器尚無法存取應用程式本身的功能。Python API 允許透過擴充嵌入式直譯器來達成這點。也就是說,嵌入式直譯器可以由應用程式所提供的例程加以擴充。雖然聽起來複雜但其實並不難。只要暫時忘記是應用程式啟動了 Python 直譯器,改以將應用程式視為一組子程序,並撰寫一些膠合程式碼讓 Python 能夠存取這些例程,就像你撰寫一般 Python 擴充一樣。例如:
staticintnumargs=0;/* Return the number of arguments of the application command line */staticPyObject*emb_numargs(PyObject*self,PyObject*args){if(!PyArg_ParseTuple(args,":numargs"))returnNULL;returnPyLong_FromLong(numargs);}staticPyMethodDefemb_module_methods[]={{"numargs",emb_numargs,METH_VARARGS,"Return the number of arguments received by the process."},{NULL,NULL,0,NULL}};staticstructPyModuleDefemb_module={.m_base=PyModuleDef_HEAD_INIT,.m_name="emb",.m_size=0,.m_methods=emb_module_methods,};staticPyObject*PyInit_emb(void){returnPyModuleDef_Init(&emb_module);}
在main() 函式的正上方插入上述程式碼。同時,在呼叫Py_Initialize() 之前插入以下兩個陳述式:
numargs=argc;PyImport_AppendInittab("emb",&PyInit_emb);
這兩行初始化numargs 變數,並讓emb.numargs() 函式可供嵌入式 Python 直譯器使用。有了這些擴充後,Python 腳本便可以執行像是以下的操作
importembprint("Number of arguments",emb.numargs())
在真實的應用程式中,這些方法會向 Python 公開應用程式的 API。
1.5.在 C++ 中嵌入 Python¶
也可以將 Python 嵌入 C++ 程式中;具體如何做取決於所使用的 C++ 系統的細節;一般來說,你需要用 C++ 撰寫主程式,並使用 C++ 編譯器來編譯和連結你的程式。不需要使用 C++ 重新編譯 Python 本身。
1.6.在類 Unix 系統下編譯和連結¶
要找到傳遞給編譯器(和連結器)的正確旗標以便將 Python 直譯器嵌入你的應用程式中,並不一定是簡單的事,特別是因為 Python 需要載入作為 C 動態擴充(.so 檔案)實作的函式庫模組,而這些模組又必須與 Python 進行連結。
要找出所需的編譯器和連結器旗標,你可以執行作為安裝過程的一部分產生的pythonX.Y-config 腳本(也可能有python3-config 腳本可用)。此腳本提供多種選項,其中以下幾個最為實用:
pythonX.Y-config--cflags會給你編譯時建議的旗標:$/opt/bin/python3.11-config--cflags-I/opt/include/python3.11 -I/opt/include/python3.11 -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall
pythonX.Y-config--ldflags--embed會給你連結時建議的旗標:$/opt/bin/python3.11-config--ldflags--embed-L/opt/lib/python3.11/config-3.11-x86_64-linux-gnu -L/opt/lib -lpython3.11 -lpthread -ldl -lutil -lm
備註
為了避免多個 Python 安裝之間產生混淆(特別是系統自帶的 Python 與自行編譯的 Python 之間),建議使用pythonX.Y-config 的絕對路徑,如上面的例子所示。
如果此程序對你不起作用(並不保證在所有類 Unix 平台上都能運作;不過我們歡迎錯誤回報),你將需要參閱系統的動態連結相關文件,並/或檢查 Python 的Makefile(可使用sysconfig.get_makefile_filename() 來找到其位置)和編譯選項。在這種情況下,sysconfig 模組是一個實用的工具,可用於以程式化方式擷取並組合所需的組態值。例如:
>>>importsysconfig>>>sysconfig.get_config_var('LIBS')'-lpthread -ldl -lutil'>>>sysconfig.get_config_var('LINKFORSHARED')'-Xlinker -export-dynamic'