1. 將 Python 嵌入到其他應用程式中

前面的章節討論瞭如何擴充套件 Python,也就是如何透過向 Python 新增 C 函式庫來擴充套件 Python 的功能。反過來也可以:透過將 Python 嵌入到你的 C/C++ 應用程式中來豐富你的應用程式。嵌入使你的應用程式能夠使用 Python 而不是 C 或 C++ 來實現應用程式的某些功能。這可以用於許多目的;一個例子是允許使用者透過編寫一些 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 起,這不是必須的,但為了向後相容,我們在此保留它。有關此宏的描述,請參閱 字串和緩衝區

設定 PyConfig.program_name 應該在 Py_InitializeFromConfig() 之前呼叫,以告知直譯器 Python 執行時庫的路徑。接下來,使用 Py_Initialize() 初始化 Python 直譯器,然後執行一個硬編碼的 Python 指令碼,該指令碼列印日期和時間。之後,Py_FinalizeEx() 呼叫關閉直譯器,然後程式結束。在實際程式中,你可能希望從其他來源(例如文字編輯器例程、檔案或資料庫)獲取 Python 指令碼。最好使用 PyRun_SimpleFile() 函式從檔案中獲取 Python 程式碼,這可以省去你分配記憶體空間和載入檔案內容的麻煩。

1.2. 超越非常高層次的嵌入:概述

高層次的介面使你能夠從應用程式中執行任意的 Python 程式碼片段,但是交換資料值至少可以說非常麻煩。如果你想要那樣做,你應該使用較低級別的呼叫。以必須編寫更多 C 程式碼為代價,你可以實現幾乎任何事情。

應該注意的是,儘管意圖不同,但擴充套件 Python 和嵌入 Python 是非常相同的活動。前面章節中討論的大多數主題仍然有效。為了說明這一點,請考慮從 Python 到 C 的擴充套件程式碼實際做了什麼

  1. 將資料值從 Python 轉換為 C,

  2. 使用轉換後的值對 C 例程執行函式呼叫,以及

  3. 將來自呼叫的資料值從 C 轉換為 Python。

當嵌入 Python 時,介面程式碼會執行

  1. 將資料值從 C 轉換為 Python,

  2. 使用轉換後的值對 Python 介面例程執行函式呼叫,以及

  3. 將來自呼叫的資料值從 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_FromString() 資料轉換例程構造的。

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 EmbMethods[] = {
    {"numargs", emb_numargs, METH_VARARGS,
     "Return the number of arguments received by the process."},
    {NULL, NULL, 0, NULL}
};

static PyModuleDef EmbModule = {
    PyModuleDef_HEAD_INIT, "emb", NULL, -1, EmbMethods,
    NULL, NULL, NULL, NULL
};

static PyObject*
PyInit_emb(void)
{
    return PyModule_Create(&EmbModule);
}

將上面的程式碼插入到 main() 函式的正上方。另外,在呼叫 Py_Initialize() 之前插入以下兩條語句

numargs = argc;
PyImport_AppendInittab("emb", &PyInit_emb);

這兩行初始化了 numargs 變數,並使嵌入式 Python 直譯器可以訪問 emb.numargs() 函式。透過這些擴充套件,Python 指令碼可以執行如下操作:

import emb
print("Number of arguments", emb.numargs())

在實際的應用程式中,這些方法會將應用程式的 API 公開給 Python。

1.5. 在 C++ 中嵌入 Python

也可以將 Python 嵌入到 C++ 程式中;具體如何操作取決於所使用的 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'