1. 在其他應用程式中嵌入 Python¶
之前的章節討論瞭如何擴充套件 Python,即透過將 C 函式庫附加到 Python 來擴充套件其功能。也可以反過來:透過在 C/C++ 應用程式中嵌入 Python 來豐富您的應用程式。嵌入功能使您的應用程式能夠以 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>
int
main(int argc, char *argv[])
{
PyStatus status;
PyConfig config;
PyConfig_InitPythonConfig(&config);
/* optional but recommended */
status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
if (PyStatus_Exception(status)) {
goto exception;
}
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) {
goto exception;
}
PyConfig_Clear(&config);
PyRun_SimpleString("from time import time,ctime\n"
"print('Today is', ctime(time()))\n");
if (Py_FinalizeEx() < 0) {
exit(120);
}
return 0;
exception:
PyConfig_Clear(&config);
Py_ExitStatusException(status);
}
備註
使用 #define PY_SSIZE_T_CLEAN
來指示在某些 API 中應使用 Py_ssize_t
而不是 int
。自 Python 3.13 以來不再需要,但我們為了向後相容性在此處保留它。有關此宏的描述,請參見 字串和緩衝區。
應在呼叫 Py_InitializeFromConfig()
之前呼叫設定 PyConfig.program_name
,以告知直譯器 Python 執行時庫的路徑。接下來,使用 Py_Initialize()
初始化 Python 直譯器,然後執行一個硬編碼的 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 反之亦然。此外,假定對引用和錯誤處理的正確使用已理解。由於這些方面與擴充套件直譯器沒有區別,您可以參考前面的章節獲取所需資訊。
1.3. 純嵌入¶
第一個程式旨在執行 Python 指令碼中的一個函式。與關於非常高階介面的部分一樣,Python 直譯器不直接與應用程式互動(但這將在下一節中改變)。
執行 Python 指令碼中定義的函式的程式碼是
#define PY_SSIZE_T_CLEAN
#include <Python.h>
int
main(int argc, char *argv[])
{
PyObject *pName, *pModule, *pFunc;
PyObject *pArgs, *pValue;
int i;
if (argc < 3) {
fprintf(stderr,"Usage: call pythonfile funcname [args]\n");
return 1;
}
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");
return 1;
}
/* 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");
return 1;
}
}
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]);
return 1;
}
if (Py_FinalizeEx() < 0) {
return 120;
}
return 0;
}
此程式碼使用 argv[1]
載入 Python 指令碼,並呼叫 argv[2]
中命名的函式。其整數引數是 argv
陣列的其他值。如果您 編譯並連結 此程式(我們稱完成的可執行檔案為 call),並使用它執行 Python 指令碼,例如
def multiply(a,b):
print("Will compute", a, "times", b)
c = 0
for i in range(0, a):
c = c + b
return c
那麼結果應該是
$ call multiply multiply 3 2
Will compute 3 times 2
Result 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 擴充套件一樣。例如
static int numargs=0;
/* Return the number of arguments of the application command line */
static PyObject*
emb_numargs(PyObject *self, PyObject *args)
{
if(!PyArg_ParseTuple(args, ":numargs"))
return NULL;
return PyLong_FromLong(numargs);
}
static PyMethodDef emb_module_methods[] = {
{"numargs", emb_numargs, METH_VARARGS,
"Return the number of arguments received by the process."},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef emb_module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "emb",
.m_size = 0,
.m_methods = emb_module_methods,
};
static PyObject*
PyInit_emb(void)
{
return PyModuleDef_Init(&emb_module);
}
將上述程式碼插入到 main()
函式的正上方。此外,在呼叫 Py_Initialize()
之前插入以下兩條語句
numargs = argc;
PyImport_AppendInittab("emb", &PyInit_emb);
這兩行初始化 numargs
變數,並使 emb.numargs()
函式可供嵌入式 Python 直譯器訪問。透過這些擴充套件,Python 指令碼可以執行以下操作
import emb
print("Number of arguments", emb.numargs())
在實際應用程式中,這些方法將向 Python 公開應用程式的 API。
1.5. 在 C++ 中嵌入 Python¶
在 C++ 程式中嵌入 Python 也是可能的;具體如何實現將取決於所使用的 C++ 系統的細節;通常您需要用 C++ 編寫主程式,並使用 C++ 編譯器編譯和連結您的程式。無需使用 C++ 重新編譯 Python 本身。
1.6. 在類 Unix 系統下編譯和連結¶
找到正確的標誌傳遞給編譯器(和連結器)以將 Python 直譯器嵌入到您的應用程式中並非易事,特別是因為 Python 需要載入實現為 C 動態擴充套件(.so
檔案)並與其連結的庫模組。
要找出所需的編譯器和連結器標誌,您可以執行作為安裝過程一部分生成的 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
模組是一個有用的工具,可以以程式設計方式提取您想要組合在一起的配置值。例如
>>> import sysconfig
>>> sysconfig.get_config_var('LIBS')
'-lpthread -ldl -lutil'
>>> sysconfig.get_config_var('LINKFORSHARED')
'-Xlinker -export-dynamic'