1. 使用 C 或 C++ 擴充套件 Python

如果您知道如何使用 C 程式設計,那麼向 Python 新增新的內建模組非常容易。這種擴充套件模組可以做兩件不能直接在 Python 中完成的事情:它們可以實現新的內建物件型別,並且可以呼叫 C 庫函式和系統呼叫。

為了支援擴充套件,Python API(應用程式程式設計介面)定義了一組函式、宏和變數,這些函式、宏和變數提供了對 Python 執行時系統的大多數方面的訪問。透過包含標頭檔案 "Python.h",Python API 被合併到 C 原始檔中。

擴充套件模組的編譯取決於其預期用途以及您的系統設定;詳細資訊將在後面的章節中給出。

注意

C 擴充套件介面是 CPython 特有的,擴充套件模組不能在其他 Python 實現上工作。在許多情況下,可以避免編寫 C 擴充套件並保持對其他實現的可移植性。例如,如果您的用例是呼叫 C 庫函式或系統呼叫,您應該考慮使用 ctypes 模組或 cffi 庫,而不是編寫自定義 C 程式碼。這些模組允許您編寫 Python 程式碼來與 C 程式碼互動,並且在 Python 的不同實現之間比編寫和編譯 C 擴充套件模組更具可移植性。

1.1. 一個簡單的例子

讓我們建立一個名為 spam 的擴充套件模組(Monty Python 粉絲最喜歡的食物……),假設我們要為 C 庫函式 system() [1] 建立一個 Python 介面。此函式接受一個以 null 結尾的字串作為引數,並返回一個整數。我們希望這個函式可以從 Python 中呼叫,如下所示

>>> import spam
>>> status = spam.system("ls -l")

首先建立一個檔案 spammodule.c。(歷史上,如果一個模組名為 spam,則包含其實現的 C 檔名為 spammodule.c;如果模組名稱很長,例如 spammify,則模組名稱可以只是 spammify.c。)

我們檔案的前兩行可以是

#define PY_SSIZE_T_CLEAN
#include <Python.h>

這會引入 Python API(您可以新增一條註釋,描述模組的用途,如果您喜歡,還可以新增版權宣告)。

注意

由於 Python 可能會定義一些預處理器定義,這些定義會影響某些系統上的標準標頭檔案,因此您必須在包含任何標準標頭檔案之前包含 Python.h

#define PY_SSIZE_T_CLEAN 用於指示在某些 API 中應使用 Py_ssize_t 而不是 int。自 Python 3.13 以來,這不是必需的,但我們在此處保留它以實現向後相容。有關此宏的描述,請參閱 字串和緩衝區

Python.h 定義的所有使用者可見的符號都帶有字首 PyPY,除了在標準標頭檔案中定義的符號。為了方便起見,並且由於 Python 直譯器廣泛使用它們,"Python.h" 包含了一些標準標頭檔案:<stdio.h><string.h><errno.h><stdlib.h>。如果後一個頭檔案在您的系統上不存在,它會直接宣告函式 malloc()free()realloc()

接下來,我們新增到模組檔案中的是 C 函式,當 Python 表示式 spam.system(string) 被求值時會呼叫該函式(我們很快就會看到它是如何被呼叫的)

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 PyLong_FromLong(sts);
}

Python 中的引數列表(例如,單個表示式 "ls -l")到傳遞給 C 函式的引數之間有一個直接的轉換。C 函式始終有兩個引數,通常命名為 selfargs

self 引數指向模組級函式的模組物件;對於方法,它將指向物件例項。

args 引數將是指向包含引數的 Python 元組物件的指標。元組的每個專案對應於呼叫引數列表中的一個引數。引數是 Python 物件 —— 為了在我們的 C 函式中對它們執行任何操作,我們必須將它們轉換為 C 值。Python API 中的函式 PyArg_ParseTuple() 檢查引數型別,並將它們轉換為 C 值。它使用一個模板字串來確定引數的必需型別,以及用於儲存轉換值的 C 變數的型別。稍後會詳細介紹。

如果所有引數都具有正確的型別,並且其元件已儲存在傳遞其地址的變數中,則 PyArg_ParseTuple() 返回 true(非零)。如果傳遞了無效的引數列表,則返回 false(零)。在後一種情況下,它還會引發一個適當的異常,以便呼叫函式可以立即返回 NULL(正如我們在示例中看到的那樣)。

1.2. 插曲:錯誤和異常

Python 直譯器中一個重要的約定如下:當函式失敗時,它應該設定一個異常條件並返回一個錯誤值(通常是 -1NULL 指標)。異常資訊儲存在直譯器執行緒狀態的三個成員中。如果沒有異常,這些成員為 NULL。否則,它們是 Python 元組成員的 C 等效項,該元組由 sys.exc_info() 返回。這些是異常型別、異常例項和回溯物件。瞭解它們對於理解錯誤是如何傳遞的非常重要。

Python API 定義了許多用於設定各種型別異常的函式。

最常見的是 PyErr_SetString()。它的引數是一個異常物件和一個 C 字串。異常物件通常是一個預定義的物件,例如 PyExc_ZeroDivisionError。C 字串表示錯誤的原因,並轉換為 Python 字串物件,並存儲為異常的“關聯值”。

另一個有用的函式是 PyErr_SetFromErrno(),它只接受一個異常引數,並透過檢查全域性變數 errno 來構造關聯的值。最通用的函式是 PyErr_SetObject(),它接受兩個物件引數,即異常及其關聯的值。您不需要對傳遞給這些函式的物件呼叫 Py_INCREF()

您可以使用 PyErr_Occurred() 來非破壞性地測試是否已設定異常。這將返回當前的異常物件,如果沒有發生異常,則返回 NULL。通常,您不需要呼叫 PyErr_Occurred() 來檢視函式呼叫中是否發生了錯誤,因為您應該能夠從返回值中判斷出來。

當一個呼叫另一個函式 g 的函式 f 檢測到後者失敗時,f 本身應該返回一個錯誤值(通常為 NULL-1)。它應該呼叫 PyErr_* 函式之一 —— g 已經呼叫了一個。然後,f 的呼叫者也應該向呼叫者返回一個錯誤指示,同樣呼叫 PyErr_*,依此類推 —— 錯誤的更詳細原因已經由首先檢測到它的函式報告了。一旦錯誤到達 Python 直譯器的主迴圈,它就會中止當前正在執行的 Python 程式碼,並嘗試查詢由 Python 程式設計師指定的異常處理程式。

(在某些情況下,模組實際上可以透過呼叫另一個 PyErr_* 函式來給出更詳細的錯誤訊息,在這種情況下這樣做是可以的。但是,作為一般規則,這是不必要的,並且可能會導致有關錯誤原因的資訊丟失:大多數操作可能會因各種原因而失敗。)

要忽略函式呼叫失敗而設定的異常,必須透過呼叫 PyErr_Clear() 來顯式清除異常條件。C 程式碼應該呼叫 PyErr_Clear() 的唯一情況是它不想將錯誤傳遞給直譯器,而是想完全由自己處理(可能是透過嘗試其他操作,或者假裝沒有發生任何錯誤)。

每次失敗的 malloc() 呼叫都必須轉換為異常 —— malloc()(或 realloc())的直接呼叫者必須呼叫 PyErr_NoMemory() 並返回一個失敗指示符。所有物件建立函式(例如,PyLong_FromLong())已經執行此操作,因此此註釋僅與直接呼叫 malloc() 的人相關。

另請注意,除了 PyArg_ParseTuple() 及其朋友之外,返回整數狀態的函式通常返回正值或零表示成功,返回 -1 表示失敗,就像 Unix 系統呼叫一樣。

最後,請注意在返回錯誤指示符時清理垃圾(透過為您已建立的物件呼叫 Py_XDECREF()Py_DECREF())!

選擇引發哪個異常完全由您決定。有與所有內建 Python 異常相對應的預先宣告的 C 物件,例如 PyExc_ZeroDivisionError,您可以直接使用它們。當然,您應該明智地選擇異常 —— 不要使用 PyExc_TypeError 來表示無法開啟檔案(那可能應該是 PyExc_OSError)。如果引數列表出現問題,PyArg_ParseTuple() 函式通常會引發 PyExc_TypeError。如果您的引數的值必須在特定範圍內或必須滿足其他條件,則 PyExc_ValueError 是合適的。

您還可以定義一個對您的模組唯一的新的異常。為此,您通常在檔案的開頭宣告一個靜態物件變數

static PyObject *SpamError;

並在您的模組的初始化函式(PyInit_spam())中使用異常物件對其進行初始化

PyMODINIT_FUNC
PyInit_spam(void)
{
    PyObject *m;

    m = PyModule_Create(&spammodule);
    if (m == NULL)
        return NULL;

    SpamError = PyErr_NewException("spam.error", NULL, NULL);
    if (PyModule_AddObjectRef(m, "error", SpamError) < 0) {
        Py_CLEAR(SpamError);
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

請注意,異常物件的 Python 名稱是 spam.errorPyErr_NewException() 函式可能會建立一個基類為 Exception 的類(除非傳入另一個類而不是 NULL),如 內建異常 中所述。

另請注意,SpamError 變數保留對新建立的異常類的引用;這是故意的!由於異常可能會被外部程式碼從模組中刪除,因此需要對該類擁有的引用以確保它不會被丟棄,從而導致 SpamError 成為懸空指標。如果它成為懸空指標,則引發異常的 C 程式碼可能會導致核心轉儲或其他意外的副作用。

我們將在本示例後面討論 PyMODINIT_FUNC 作為函式返回型別的使用。

可以使用以下所示的對 PyErr_SetString() 的呼叫在您的擴充套件模組中引發 spam.error 異常

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = system(command);
    if (sts < 0) {
        PyErr_SetString(SpamError, "System command failed");
        return NULL;
    }
    return PyLong_FromLong(sts);
}

1.3. 回到示例

回到我們的示例函式,您現在應該能夠理解此語句

if (!PyArg_ParseTuple(args, "s", &command))
    return NULL;

如果在引數列表中檢測到錯誤,它會返回 NULL(返回物件指標的函式的錯誤指示符),這依賴於 PyArg_ParseTuple() 設定的異常。否則,引數的字串值已複製到區域性變數 command。這是一個指標賦值,您不應該修改它指向的字串(因此在標準 C 中,變數 command 應該正確地宣告為 const char *command)。

下一條語句是對 Unix 函式 system() 的呼叫,並將我們剛剛從 PyArg_ParseTuple() 獲取的字串傳遞給它

sts = system(command);

我們的 spam.system() 函式必須將 sts 的值作為 Python 物件返回。這是使用函式 PyLong_FromLong() 完成的。

return PyLong_FromLong(sts);

在這種情況下,它將返回一個整數物件。(是的,即使整數在 Python 中也是堆上的物件!)

如果您的 C 函式不返回有用的引數(返回 void 的函式),則相應的 Python 函式必須返回 None。您需要使用此習慣用法來執行此操作(它由 Py_RETURN_NONE 宏實現)

Py_INCREF(Py_None);
return Py_None;

Py_None 是特殊 Python 物件 None 的 C 名稱。它是一個真正的 Python 物件,而不是 NULL 指標,正如我們所見,NULL 指標在大多數情況下表示“錯誤”。

1.4. 模組的方法表和初始化函式

我承諾會展示如何在 Python 程式中呼叫 spam_system() 函式。首先,我們需要在“方法表”中列出它的名稱和地址。

static PyMethodDef SpamMethods[] = {
    ...
    {"system",  spam_system, METH_VARARGS,
     "Execute a shell command."},
    ...
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

請注意第三個條目 (METH_VARARGS)。這是一個標誌,告訴直譯器要用於 C 函式的呼叫約定。它通常應始終為 METH_VARARGSMETH_VARARGS | METH_KEYWORDS;值為 0 表示使用 PyArg_ParseTuple() 的過時變體。

當僅使用 METH_VARARGS 時,該函式應期望 Python 級別的引數作為元組傳入,該元組可以透過 PyArg_ParseTuple() 進行解析;有關此函式的更多資訊將在下面提供。

如果應將關鍵字引數傳遞給函式,則可以在第三個欄位中設定 METH_KEYWORDS 位。在這種情況下,C 函式應接受第三個 PyObject * 引數,該引數將是一個關鍵字字典。使用 PyArg_ParseTupleAndKeywords() 來解析此類函式的引數。

方法表必須在模組定義結構中被引用。

static struct PyModuleDef spammodule = {
    PyModuleDef_HEAD_INIT,
    "spam",   /* name of module */
    spam_doc, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    SpamMethods
};

反過來,此結構必須在模組的初始化函式中傳遞給直譯器。初始化函式必須命名為 PyInit_name(),其中 *name* 是模組的名稱,並且應該是模組檔案中定義的唯一非 static 項。

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModule_Create(&spammodule);
}

請注意,PyMODINIT_FUNC 將函式宣告為 PyObject * 返回型別,宣告平臺所需的任何特殊連結宣告,並且對於 C++,將該函式宣告為 extern "C"

當 Python 程式第一次匯入模組 spam 時,將呼叫 PyInit_spam()。(有關嵌入 Python 的註釋,請參見下文。)它呼叫 PyModule_Create(),該函式返回一個模組物件,並根據模組定義中找到的表(PyMethodDef 結構的陣列)將內建函式物件插入到新建立的模組中。PyModule_Create() 返回指向它建立的模組物件的指標。對於某些錯誤,它可能會因致命錯誤而中止,或者如果模組無法令人滿意地初始化,則返回 NULL。初始化函式必須將其模組物件返回給呼叫方,以便隨後將其插入到 sys.modules 中。

當嵌入 Python 時,除非 PyImport_Inittab 表中有條目,否則不會自動呼叫 PyInit_spam() 函式。要將模組新增到初始化表,請使用 PyImport_AppendInittab(),可以選擇後跟模組的匯入。

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
main(int argc, char *argv[])
{
    PyStatus status;
    PyConfig config;
    PyConfig_InitPythonConfig(&config);

    /* Add a built-in module, before Py_Initialize */
    if (PyImport_AppendInittab("spam", PyInit_spam) == -1) {
        fprintf(stderr, "Error: could not extend in-built modules table\n");
        exit(1);
    }

    /* Pass argv[0] to the Python interpreter */
    status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
    if (PyStatus_Exception(status)) {
        goto exception;
    }

    /* Initialize the Python interpreter.  Required.
       If this step fails, it will be a fatal error. */
    status = Py_InitializeFromConfig(&config);
    if (PyStatus_Exception(status)) {
        goto exception;
    }
    PyConfig_Clear(&config);

    /* Optionally import the module; alternatively,
       import can be deferred until the embedded script
       imports it. */
    PyObject *pmodule = PyImport_ImportModule("spam");
    if (!pmodule) {
        PyErr_Print();
        fprintf(stderr, "Error: could not import module 'spam'\n");
    }

    // ... use Python C API here ...

    return 0;

  exception:
     PyConfig_Clear(&config);
     Py_ExitStatusException(status);
}

注意

sys.modules 中刪除條目或將編譯後的模組匯入程序中的多個直譯器(或在沒有中間 exec() 的情況下執行 fork() 之後)可能會給某些擴充套件模組帶來問題。擴充套件模組作者在初始化內部資料結構時應謹慎行事。

Python 原始碼發行版中包含了一個更實質性的示例模組,即 Modules/xxmodule.c。此檔案可以用作模板或僅作為示例閱讀。

注意

與我們的 spam 示例不同,xxmodule 使用 *多階段初始化*(在 Python 3.5 中新增),其中 PyModuleDef 結構從 PyInit_spam 返回,並且模組的建立留給匯入機制。有關多階段初始化的詳細資訊,請參閱 PEP 489

1.5. 編譯和連結

在使用新的擴充套件之前,還需要做兩件事:將其與 Python 系統編譯和連結。如果使用動態載入,則詳細資訊可能取決於系統使用的動態載入樣式;有關此的更多資訊,請參閱有關構建擴充套件模組的章節(第 構建 C 和 C++ 擴充套件 章)以及僅適用於在 Windows 上構建的其他資訊(第 在 Windows 上構建 C 和 C++ 擴充套件 章)。

如果無法使用動態載入,或者想使模組成為 Python 直譯器的永久組成部分,則必須更改配置設定並重新構建直譯器。幸運的是,這在 Unix 上非常簡單:只需將檔案(例如 spammodule.c)放置在解壓縮的原始碼發行版的 Modules/ 目錄中,然後在檔案 Modules/Setup.local 中新增一行描述檔案。

spam spammodule.o

並透過在頂層目錄中執行 make 來重建直譯器。你也可以在 Modules/ 子目錄中執行 make,但是你必須先透過執行 ‘make Makefile’ 來重建那裡的 Makefile。(每次更改 Setup 檔案時,都需要這樣做。)

如果模組需要連結其他庫,也可以在配置檔案中的行上列出這些庫,例如

spam spammodule.o -lX11

1.6. 從 C 呼叫 Python 函式

到目前為止,我們專注於使 C 函式可以從 Python 中呼叫。反之亦然:從 C 呼叫 Python 函式也很有用。對於支援所謂“回撥”函式的庫尤其如此。如果 C 介面使用回撥,則等效的 Python 通常需要向 Python 程式設計師提供回撥機制;該實現將需要從 C 回撥中呼叫 Python 回撥函式。也可以想象其他用途。

幸運的是,Python 直譯器可以輕鬆地遞迴呼叫,並且有一個標準的介面來呼叫 Python 函式。(我不會詳細介紹如何使用特定字串作為輸入來呼叫 Python 解析器 — 如果你感興趣,請檢視 Python 原始碼中的 Modules/main.c-c 命令列選項的實現。)

呼叫 Python 函式很容易。首先,Python 程式必須以某種方式將 Python 函式物件傳遞給你。你應該提供一個函式(或其他介面)來執行此操作。當呼叫此函式時,將指向 Python 函式物件的指標儲存在全域性變數中(注意使用 Py_INCREF() 增加其引用計數!)— 或你認為合適的任何地方。例如,以下函式可能是模組定義的一部分。

static PyObject *my_callback = NULL;

static PyObject *
my_set_callback(PyObject *dummy, PyObject *args)
{
    PyObject *result = NULL;
    PyObject *temp;

    if (PyArg_ParseTuple(args, "O:set_callback", &temp)) {
        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError, "parameter must be callable");
            return NULL;
        }
        Py_XINCREF(temp);         /* Add a reference to new callback */
        Py_XDECREF(my_callback);  /* Dispose of previous callback */
        my_callback = temp;       /* Remember new callback */
        /* Boilerplate to return "None" */
        Py_INCREF(Py_None);
        result = Py_None;
    }
    return result;
}

必須使用 METH_VARARGS 標誌向直譯器註冊此函式;這在 模組的方法表和初始化函式 一節中進行了描述。PyArg_ParseTuple() 函式及其引數在 在擴充套件函式中提取引數 一節中進行了說明。

Py_XINCREF()Py_XDECREF() 增加/減少物件的引用計數,並且在存在 NULL 指標的情況下是安全的(但請注意,在此上下文中,*temp* 不會為 NULL)。有關它們的更多資訊,請參見 引用計數 一節。

稍後,當需要呼叫該函式時,你可以呼叫 C 函式 PyObject_CallObject()。此函式有兩個引數,都是指向任意 Python 物件的指標:Python 函式和引數列表。引數列表必須始終是元組物件,其長度是引數的數量。要呼叫不帶任何引數的 Python 函式,請傳入 NULL 或一個空元組;要呼叫帶一個引數的函式,請傳入一個單例元組。Py_BuildValue() 當其格式字串由括號之間的零個或多個格式程式碼組成時,返回一個元組。例如

int arg;
PyObject *arglist;
PyObject *result;
...
arg = 123;
...
/* Time to call the callback */
arglist = Py_BuildValue("(i)", arg);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);

PyObject_CallObject() 返回一個 Python 物件指標:這是 Python 函式的返回值。PyObject_CallObject() 在其引數方面是“引用計數中性”的。在示例中,建立了一個新的元組作為引數列表,它在 PyObject_CallObject() 呼叫之後立即被 Py_DECREF() 減少引用計數。

PyObject_CallObject() 的返回值是“新的”:要麼是一個全新的物件,要麼是引用計數已增加的現有物件。因此,除非你想將它儲存在全域性變數中,否則你應該以某種方式 Py_DECREF() 結果,即使(尤其!)你對它的值不感興趣。

然而,在你這樣做之前,重要的是檢查返回值是否為 NULL。如果它是,則 Python 函式透過引發異常而終止。如果呼叫 PyObject_CallObject() 的 C 程式碼是從 Python 呼叫的,它現在應該向其 Python 呼叫者返回一個錯誤指示,以便直譯器可以列印堆疊跟蹤,或者呼叫 Python 程式碼可以處理該異常。如果這是不可能或不可取的,則應透過呼叫 PyErr_Clear() 來清除異常。例如

if (result == NULL)
    return NULL; /* Pass error back */
...use result...
Py_DECREF(result);

根據所需的 Python 回撥函式介面,你可能還需要為 PyObject_CallObject() 提供引數列表。在某些情況下,引數列表也由 Python 程式透過指定回撥函式的相同介面提供。然後,可以像函式物件一樣儲存和使用它。在其他情況下,你可能必須構造一個新的元組來作為引數列表傳遞。最簡單的方法是呼叫 Py_BuildValue()。例如,如果要傳遞一個整數事件程式碼,可以使用以下程式碼

PyObject *arglist;
...
arglist = Py_BuildValue("(l)", eventcode);
result = PyObject_CallObject(my_callback, arglist);
Py_DECREF(arglist);
if (result == NULL)
    return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);

請注意 Py_DECREF(arglist) 的位置,它在呼叫之後,錯誤檢查之前!還要注意,嚴格來說,這段程式碼並不完整:Py_BuildValue() 可能會耗盡記憶體,應該檢查這一點。

你還可以透過使用 PyObject_Call() 來使用關鍵字引數呼叫函式,該函式支援引數和關鍵字引數。與上面的示例一樣,我們使用 Py_BuildValue() 來構造字典。

PyObject *dict;
...
dict = Py_BuildValue("{s:i}", "name", val);
result = PyObject_Call(my_callback, NULL, dict);
Py_DECREF(dict);
if (result == NULL)
    return NULL; /* Pass error back */
/* Here maybe use the result */
Py_DECREF(result);

1.7. 在擴充套件函式中提取引數

PyArg_ParseTuple() 函式宣告如下

int PyArg_ParseTuple(PyObject *arg, const char *format, ...);

arg 引數必須是一個包含從 Python 傳遞給 C 函式的引數列表的元組物件。format 引數必須是一個格式字串,其語法在 Python/C API 參考手冊的 解析引數和構建值 中進行了解釋。其餘引數必須是由格式字串確定的型別的變數的地址。

請注意,雖然 PyArg_ParseTuple() 檢查 Python 引數是否具有所需的型別,但它無法檢查傳遞給呼叫的 C 變數的地址的有效性:如果你在那裡犯了錯誤,你的程式碼可能會崩潰,或者至少會覆蓋記憶體中的隨機位。所以要小心!

請注意,提供給呼叫者的任何 Python 物件引用都是借用的引用;不要減少它們的引用計數!

一些示例呼叫

#define PY_SSIZE_T_CLEAN
#include <Python.h>
int ok;
int i, j;
long k, l;
const char *s;
Py_ssize_t size;

ok = PyArg_ParseTuple(args, ""); /* No arguments */
    /* Python call: f() */
ok = PyArg_ParseTuple(args, "s", &s); /* A string */
    /* Possible Python call: f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* Two longs and a string */
    /* Possible Python call: f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
    /* A pair of ints and a string, whose size is also returned */
    /* Possible Python call: f((1, 2), 'three') */
{
    const char *file;
    const char *mode = "r";
    int bufsize = 0;
    ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
    /* A string, and optionally another string and an integer */
    /* Possible Python calls:
       f('spam')
       f('spam', 'w')
       f('spam', 'wb', 100000) */
}
{
    int left, top, right, bottom, h, v;
    ok = PyArg_ParseTuple(args, "((ii)(ii))(ii)",
             &left, &top, &right, &bottom, &h, &v);
    /* A rectangle and a point */
    /* Possible Python call:
       f(((0, 0), (400, 300)), (10, 10)) */
}
{
    Py_complex c;
    ok = PyArg_ParseTuple(args, "D:myfunction", &c);
    /* a complex, also providing a function name for errors */
    /* Possible Python call: myfunction(1+2j) */
}

1.8. 擴充套件函式的關鍵字引數

PyArg_ParseTupleAndKeywords() 函式宣告如下

int PyArg_ParseTupleAndKeywords(PyObject *arg, PyObject *kwdict,
                                const char *format, char * const *kwlist, ...);

argformat 引數與 PyArg_ParseTuple() 函式的引數相同。kwdict 引數是從 Python 執行時接收的關鍵字字典作為第三個引數。kwlist 引數是一個以 NULL 結尾的字串列表,用於標識引數;名稱從左到右與 format 中的型別資訊匹配。成功時,PyArg_ParseTupleAndKeywords() 返回 true,否則返回 false 並引發適當的異常。

注意

使用關鍵字引數時,無法解析巢狀元組!在 kwlist 中不存在的傳遞的關鍵字引數將導致引發 TypeError

這是一個使用關鍵字的示例模組,基於 Geoff Philbrick (philbrick@hks.com) 的示例

#define PY_SSIZE_T_CLEAN
#include <Python.h>

static PyObject *
keywdarg_parrot(PyObject *self, PyObject *args, PyObject *keywds)
{
    int voltage;
    const char *state = "a stiff";
    const char *action = "voom";
    const char *type = "Norwegian Blue";

    static char *kwlist[] = {"voltage", "state", "action", "type", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, keywds, "i|sss", kwlist,
                                     &voltage, &state, &action, &type))
        return NULL;

    printf("-- This parrot wouldn't %s if you put %i Volts through it.\n",
           action, voltage);
    printf("-- Lovely plumage, the %s -- It's %s!\n", type, state);

    Py_RETURN_NONE;
}

static PyMethodDef keywdarg_methods[] = {
    /* The cast of the function is necessary since PyCFunction values
     * only take two PyObject* parameters, and keywdarg_parrot() takes
     * three.
     */
    {"parrot", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
     "Print a lovely skit to standard output."},
    {NULL, NULL, 0, NULL}   /* sentinel */
};

static struct PyModuleDef keywdargmodule = {
    PyModuleDef_HEAD_INIT,
    "keywdarg",
    NULL,
    -1,
    keywdarg_methods
};

PyMODINIT_FUNC
PyInit_keywdarg(void)
{
    return PyModule_Create(&keywdargmodule);
}

1.9. 構建任意值

此函式是 PyArg_ParseTuple() 的對應函式。它的宣告如下

PyObject *Py_BuildValue(const char *format, ...);

它識別一組與 PyArg_ParseTuple() 識別的格式單元類似的格式單元,但引數(是函式的輸入,而不是輸出)不能是指標,只能是值。它返回一個新的 Python 物件,適合從 Python 呼叫的 C 函式返回。

PyArg_ParseTuple() 的一個區別:後者要求其第一個引數為元組(因為 Python 引數列表在內部始終表示為元組),而 Py_BuildValue() 並不總是構建元組。僅當其格式字串包含兩個或多個格式單元時,它才會構建元組。如果格式字串為空,則返回 None;如果它只包含一個格式單元,則返回該格式單元描述的任何物件。要強制它返回大小為 0 或 1 的元組,請將格式字串括在括號中。

示例(左側是呼叫,右側是生成的 Python 值)

Py_BuildValue("")                        None
Py_BuildValue("i", 123)                  123
Py_BuildValue("iii", 123, 456, 789)      (123, 456, 789)
Py_BuildValue("s", "hello")              'hello'
Py_BuildValue("y", "hello")              b'hello'
Py_BuildValue("ss", "hello", "world")    ('hello', 'world')
Py_BuildValue("s#", "hello", 4)          'hell'
Py_BuildValue("y#", "hello", 4)          b'hell'
Py_BuildValue("()")                      ()
Py_BuildValue("(i)", 123)                (123,)
Py_BuildValue("(ii)", 123, 456)          (123, 456)
Py_BuildValue("(i,i)", 123, 456)         (123, 456)
Py_BuildValue("[i,i]", 123, 456)         [123, 456]
Py_BuildValue("{s:i,s:i}",
              "abc", 123, "def", 456)    {'abc': 123, 'def': 456}
Py_BuildValue("((ii)(ii)) (ii)",
              1, 2, 3, 4, 5, 6)          (((1, 2), (3, 4)), (5, 6))

1.10. 引用計數

在像 C 或 C++ 這樣的語言中,程式設計師負責在堆上動態分配和釋放記憶體。在 C 中,這是使用函式 malloc()free() 完成的。在 C++ 中,運算子 newdelete 使用的含義基本相同,我們將以下討論限制在 C 的情況。

使用 malloc() 分配的每個記憶體塊最終都應透過精確地呼叫一次 free() 返回到可用記憶體池。在正確的時間呼叫 free() 非常重要。如果忘記了某個塊的地址,但沒有為其呼叫 free(),則在其程式終止之前,它佔用的記憶體無法重用。這稱為記憶體洩漏。另一方面,如果程式為某個塊呼叫 free(),然後繼續使用該塊,則會與透過另一個 malloc() 呼叫重用該塊產生衝突。這稱為使用已釋放的記憶體。它的後果與引用未初始化的資料相同——核心轉儲、錯誤的結果、神秘的崩潰。

記憶體洩漏的常見原因是程式碼中不尋常的路徑。例如,一個函式可能會分配一塊記憶體,進行一些計算,然後再次釋放該記憶體塊。現在,函式需求的變化可能會在計算中新增一個測試,以檢測錯誤條件並提前從函式返回。在進行這種提前退出時,很容易忘記釋放已分配的記憶體塊,尤其是在稍後將其新增到程式碼中時。這種洩漏一旦引入,通常會長時間未被發現:錯誤退出僅在所有呼叫的一小部分中發生,並且大多數現代機器都有大量的虛擬記憶體,因此洩漏僅在頻繁使用洩漏函式的長時間執行的程序中變得明顯。因此,透過制定編碼約定或策略來最大限度地減少此類錯誤,對於防止洩漏的發生至關重要。

由於 Python 大量使用 malloc()free(),它也需要一種策略來避免記憶體洩漏以及使用已釋放的記憶體。選擇的方法稱為引用計數。原理很簡單:每個物件都包含一個計數器,當對該物件的引用儲存在某處時,該計數器會遞增,而當對該物件的引用被刪除時,該計數器會遞減。當計數器達到零時,對該物件的最後一次引用已被刪除,並且該物件被釋放。

另一種策略稱為自動垃圾回收。(有時,引用計數也被稱為垃圾回收策略,因此我使用“自動”來區分兩者。)自動垃圾回收的最大優點是使用者不需要顯式呼叫 free()。(另一個聲稱的優點是提高了速度或記憶體使用率——但這並不是確鑿的事實。)缺點是對於 C 來說,沒有真正可移植的自動垃圾回收器,而引用計數可以可移植地實現(只要函式 malloc()free() 可用——C 標準保證了這一點)。也許有一天,C 會出現足夠可移植的自動垃圾回收器。在那之前,我們必須忍受引用計數。

雖然 Python 使用傳統的引用計數實現,但它還提供了一個迴圈檢測器來檢測引用迴圈。這使得應用程式無需擔心建立直接或間接的迴圈引用;這是僅使用引用計數實現的垃圾回收的弱點。引用迴圈由包含(可能間接)對自身引用的物件組成,因此迴圈中的每個物件的引用計數都為非零。典型的引用計數實現無法回收屬於引用迴圈中任何物件或從迴圈中的物件引用的記憶體,即使沒有對迴圈本身的進一步引用。

迴圈檢測器能夠檢測垃圾迴圈並回收它們。gc 模組公開了一種執行檢測器的方法(collect() 函式),以及配置介面和在執行時停用檢測器的能力。

1.10.1. Python 中的引用計數

有兩個宏,Py_INCREF(x)Py_DECREF(x),用於處理引用計數的遞增和遞減。Py_DECREF() 還在計數達到零時釋放物件。為了靈活性,它不會直接呼叫 free(),而是透過物件型別物件中的函式指標進行呼叫。為此(以及其他目的),每個物件還包含一個指向其型別物件的指標。

現在剩下的最大問題是:何時使用 Py_INCREF(x)Py_DECREF(x)?讓我們首先介紹一些術語。沒有人“擁有”一個物件;但是,您可以擁有對物件的引用。物件的引用計數現在定義為擁有的對其的引用的數量。引用的所有者負責在不再需要該引用時呼叫 Py_DECREF()。引用的所有權可以轉移。有三種方法可以處理擁有的引用:傳遞它、儲存它或呼叫 Py_DECREF()。忘記處理擁有的引用會建立記憶體洩漏。

也可以借用對物件的引用 [2]。引用的借用者不應呼叫 Py_DECREF()。借用者不得持有物件的時間超過借用物件的所有者。在所有者處理完借用引用後使用它有使用已釋放記憶體的風險,應完全避免這種情況 [3]

與擁有引用相比,借用引用的優點是您無需在程式碼的所有可能路徑上處理引用的釋放——換句話說,使用借用引用時,您不會在提前退出時面臨洩漏的風險。與擁有引用相比,借用引用的缺點是一些微妙的情況,看似正確的程式碼中,在借用引用的所有者實際上已處理完該引用後,仍然可以使用借用的引用。

可以透過呼叫 Py_INCREF() 將借用引用更改為擁有引用。這不會影響借用引用的所有者的狀態——它會建立一個新的擁有引用,並賦予完整的擁有者責任(新所有者必須正確處理該引用,以及先前的所有者)。

1.10.2. 所有權規則

每當物件引用傳入或傳出函式時,是否隨引用傳遞所有權都是函式介面規範的一部分。

大多數返回物件引用的函式都會隨引用傳遞所有權。特別是,所有其功能是建立新物件的函式(例如 PyLong_FromLong()Py_BuildValue())都會將所有權傳遞給接收者。即使該物件實際上不是新的,您仍然會收到對該物件的新引用的所有權。例如,PyLong_FromLong() 維護常用值的快取,並且可以返回對快取項的引用。

許多從其他物件提取物件的函式也會隨引用轉移所有權,例如 PyObject_GetAttrString()。但是,這裡的情況不太明確,因為一些常用例程是例外:PyTuple_GetItem()PyList_GetItem()PyDict_GetItem()PyDict_GetItemString() 都會返回您從元組、列表或字典中借用的引用。

函式 PyImport_AddModule() 也返回一個借用的引用,即使它實際上可能會建立它返回的物件:這是可能的,因為對該物件的擁有引用儲存在 sys.modules 中。

當你將物件引用傳遞給另一個函式時,通常情況下,該函式會借用你的引用——如果它需要儲存該引用,它會使用 Py_INCREF() 使其成為一個獨立的擁有者。但有兩個重要的例外:PyTuple_SetItem()PyList_SetItem()。這些函式會接管傳遞給它們的項的所有權——即使它們失敗了!(請注意,PyDict_SetItem() 及其同類函式不會接管所有權——它們是“正常的”。)

當從 Python 呼叫 C 函式時,它會從呼叫者那裡借用對其引數的引用。呼叫者擁有對該物件的引用,因此借用的引用的生命週期保證在函式返回之前有效。只有當必須儲存或傳遞此類借用的引用時,才必須透過呼叫 Py_INCREF() 將其轉換為擁有的引用。

從 Python 呼叫的 C 函式返回的物件引用必須是一個擁有的引用——所有權從函式轉移到其呼叫者。

1.10.3. 薄冰

在一些情況下,看似無害地使用借用的引用可能會導致問題。這些都與直譯器的隱式呼叫有關,這可能導致引用的所有者處置它。

第一個也是最重要需要了解的情況是在借用列表項的引用時,對不相關的物件使用 Py_DECREF()。例如

void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);

    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0); /* BUG! */
}

此函式首先借用對 list[0] 的引用,然後將 list[1] 替換為值 0,最後列印借用的引用。看起來無害,對吧?但事實並非如此!

讓我們跟蹤控制流進入 PyList_SetItem()。列表擁有對其所有項的引用,因此當第 1 項被替換時,它必須處置原始的第 1 項。現在假設原始的第 1 項是使用者定義類的例項,並且假設該類定義了 __del__() 方法。如果此類的例項的引用計數為 1,則處置它將呼叫其 __del__() 方法。

由於它是用 Python 編寫的,__del__() 方法可以執行任意 Python 程式碼。它是否有可能做一些事情來使 bug() 中的 item 引用失效?當然可以!假設傳遞給 bug() 的列表可以被 __del__() 方法訪問,它可以執行類似於 del list[0] 的語句,並且假設這是對該物件的最後一次引用,它將釋放與之關聯的記憶體,從而使 item 失效。

一旦知道問題的根源,解決方案就很簡單:暫時增加引用計數。該函式的正確版本如下:

void
no_bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);

    Py_INCREF(item);
    PyList_SetItem(list, 1, PyLong_FromLong(0L));
    PyObject_Print(item, stdout, 0);
    Py_DECREF(item);
}

這是一個真實的故事。舊版本的 Python 包含此 bug 的變體,有人花了很多時間在 C 偵錯程式中來弄清楚為什麼他的 __del__() 方法會失敗…

借用引用問題的第二種情況是涉及執行緒的變體。通常,Python 直譯器中的多個執行緒不會相互干擾,因為有一個全域性鎖保護 Python 的整個物件空間。但是,可以使用宏 Py_BEGIN_ALLOW_THREADS 暫時釋放此鎖,並使用 Py_END_ALLOW_THREADS 重新獲取它。這在阻塞 I/O 呼叫周圍很常見,以便在等待 I/O 完成時讓其他執行緒使用處理器。顯然,以下函式與上一個函式存在相同的問題

void
bug(PyObject *list)
{
    PyObject *item = PyList_GetItem(list, 0);
    Py_BEGIN_ALLOW_THREADS
    ...some blocking I/O call...
    Py_END_ALLOW_THREADS
    PyObject_Print(item, stdout, 0); /* BUG! */
}

1.10.4. NULL 指標

通常,將物件引用作為引數的函式不希望您向其傳遞 NULL 指標,如果您這樣做,將會導致核心轉儲(或導致稍後的核心轉儲)。返回物件引用的函式通常僅在發生異常時才返回 NULL。不測試 NULL 引數的原因是函式經常將其接收的物件傳遞給其他函式——如果每個函式都測試 NULL,則會有很多冗餘的測試,程式碼的執行速度會變慢。

最好僅在“源頭”處測試 NULL:當接收到可能為 NULL 的指標時,例如,從 malloc() 或從可能引發異常的函式返回的。

Py_INCREF()Py_DECREF() 不檢查 NULL 指標——但是,它們的變體 Py_XINCREF()Py_XDECREF() 會檢查。

用於檢查特定物件型別的宏 (Pytype_Check()) 不檢查 NULL 指標——同樣,有很多程式碼會連續呼叫其中幾個來測試物件是否符合各種不同的預期型別,並且這將產生冗餘的測試。沒有帶有 NULL 檢查的變體。

C 函式呼叫機制保證傳遞給 C 函式的引數列表(示例中的 args)永遠不會為 NULL——實際上,它保證它始終是一個元組 [4]

NULL 指標“逃逸”到 Python 使用者是一個嚴重的錯誤。

1.11. 用 C++ 編寫擴充套件

可以使用 C++ 編寫擴充套件模組。有一些限制。如果主程式(Python 直譯器)是由 C 編譯器編譯和連結的,則不能使用帶有建構函式的全域性或靜態物件。如果主程式由 C++ 編譯器連結,則這不是問題。將由 Python 直譯器呼叫的函式(特別是模組初始化函式)必須使用 extern "C" 宣告。沒有必要將 Python 標頭檔案括在 extern "C" {...} 中——如果定義了符號 __cplusplus (所有最近的 C++ 編譯器都定義了這個符號),它們已經使用這種形式。

1.12. 為擴充套件模組提供 C API

許多擴充套件模組只是提供新的函式和型別以供 Python 使用,但有時擴充套件模組中的程式碼對於其他擴充套件模組也很有用。例如,擴充套件模組可以實現一個類似於無序列表的“集合”型別。就像標準 Python 列表型別具有允許擴充套件模組建立和操作列表的 C API 一樣,這種新的集合型別應該有一組 C 函式,用於直接從其他擴充套件模組進行操作。

乍一看,這似乎很容易:只需編寫函式(當然,不要將其宣告為 static),提供適當的標頭檔案並記錄 C API。實際上,如果所有擴充套件模組始終與 Python 直譯器靜態連結,這將有效。但是,當模組用作共享庫時,一個模組中定義的符號可能對另一個模組不可見。可見性的詳細資訊取決於作業系統;某些系統對 Python 直譯器和所有擴充套件模組使用一個全域性名稱空間(例如,Windows),而另一些系統則需要在模組連結時顯式列出匯入的符號(AIX 是一個例子),或者提供不同的策略選擇(大多數 Unix)。即使符號是全域性可見的,您希望呼叫其函式的模組可能尚未載入!

因此,可移植性要求不要對符號可見性做出任何假設。這意味著擴充套件模組中的所有符號都應宣告為 static,除了模組的初始化函式之外,以避免與其他擴充套件模組發生名稱衝突(如 模組的方法表和初始化函式 一節中所討論的)。這意味著 應該 可以從其他擴充套件模組訪問的符號必須以不同的方式匯出。

Python 提供了一種特殊的機制,可以將 C 級別的資訊(指標)從一個擴充套件模組傳遞到另一個擴充套件模組:Capsules(膠囊)。Capsule 是一種 Python 資料型別,它儲存一個指標 (void*)。Capsule 只能透過其 C API 建立和訪問,但可以像任何其他 Python 物件一樣傳遞。特別是,它們可以被賦值給擴充套件模組名稱空間中的一個名稱。其他擴充套件模組隨後可以匯入該模組,檢索該名稱的值,然後從 Capsule 中檢索指標。

有很多方法可以使用 Capsules 來匯出擴充套件模組的 C API。每個函式都可以擁有自己的 Capsule,或者所有 C API 指標都可以儲存在一個數組中,該陣列的地址釋出在一個 Capsule 中。儲存和檢索指標的各種任務可以在提供程式碼的模組和客戶端模組之間以不同的方式分配。

無論您選擇哪種方法,正確命名 Capsule 都非常重要。函式 PyCapsule_New() 接受一個 name 引數 (const char*);允許傳入 NULL 名稱,但我們強烈建議您指定一個名稱。正確命名的 Capsule 提供了一定程度的執行時型別安全;沒有可行的方法來區分一個未命名的 Capsule 和另一個未命名的 Capsule。

特別是,用於公開 C API 的 Capsule 應該使用以下約定進行命名

modulename.attributename

方便函式 PyCapsule_Import() 可以很容易地載入透過 Capsule 提供的 C API,但前提是 Capsule 的名稱與此約定匹配。此行為為 C API 使用者提供了高度的確定性,即他們載入的 Capsule 包含正確的 C API。

以下示例演示了一種將大部分負擔放在匯出模組編寫者身上的方法,這適用於常用的庫模組。它將所有 C API 指標(在示例中只有一個!)儲存在 void 指標的陣列中,該陣列成為 Capsule 的值。與模組對應的標頭檔案提供了一個宏,負責匯入模組並檢索其 C API 指標;客戶端模組只需在訪問 C API 之前呼叫此宏即可。

匯出模組是對 一個簡單的例子 部分中的 spam 模組的修改。函式 spam.system() 不會直接呼叫 C 庫函式 system(),而是呼叫函式 PySpam_System(),當然在實際情況中它會做更復雜的事情(例如向每個命令新增 “spam”)。這個函式 PySpam_System() 也被匯出到其他擴充套件模組。

函式 PySpam_System() 是一個普通的 C 函式,宣告為 static,像其他所有函式一樣

static int
PySpam_System(const char *command)
{
    return system(command);
}

函式 spam_system() 以一種微不足道的方式進行了修改

static PyObject *
spam_system(PyObject *self, PyObject *args)
{
    const char *command;
    int sts;

    if (!PyArg_ParseTuple(args, "s", &command))
        return NULL;
    sts = PySpam_System(command);
    return PyLong_FromLong(sts);
}

在模組的開頭,緊隨其後的是

#include <Python.h>

必須新增兩行

#define SPAM_MODULE
#include "spammodule.h"

#define 用於告訴標頭檔案,它被包含在匯出模組中,而不是客戶端模組中。最後,模組的初始化函式必須負責初始化 C API 指標陣列

PyMODINIT_FUNC
PyInit_spam(void)
{
    PyObject *m;
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;

    m = PyModule_Create(&spammodule);
    if (m == NULL)
        return NULL;

    /* Initialize the C API pointer array */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* Create a Capsule containing the API pointer array's address */
    c_api_object = PyCapsule_New((void *)PySpam_API, "spam._C_API", NULL);

    if (PyModule_Add(m, "_C_API", c_api_object) < 0) {
        Py_DECREF(m);
        return NULL;
    }

    return m;
}

請注意,PySpam_API 被宣告為 static;否則,當 PyInit_spam() 終止時,指標陣列將消失!

大部分工作在標頭檔案 spammodule.h 中,該檔案如下所示

#ifndef Py_SPAMMODULE_H
#define Py_SPAMMODULE_H
#ifdef __cplusplus
extern "C" {
#endif

/* Header file for spammodule */

/* C API functions */
#define PySpam_System_NUM 0
#define PySpam_System_RETURN int
#define PySpam_System_PROTO (const char *command)

/* Total number of C API pointers */
#define PySpam_API_pointers 1


#ifdef SPAM_MODULE
/* This section is used when compiling spammodule.c */

static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;

#else
/* This section is used in modules that use spammodule's API */

static void **PySpam_API;

#define PySpam_System \
 (*(PySpam_System_RETURN (*)PySpam_System_PROTO) PySpam_API[PySpam_System_NUM])

/* Return -1 on error, 0 on success.
 * PyCapsule_Import will set an exception if there's an error.
 */
static int
import_spam(void)
{
    PySpam_API = (void **)PyCapsule_Import("spam._C_API", 0);
    return (PySpam_API != NULL) ? 0 : -1;
}

#endif

#ifdef __cplusplus
}
#endif

#endif /* !defined(Py_SPAMMODULE_H) */

為了能夠訪問函式 PySpam_System(),客戶端模組必須做的就是在其初始化函式中呼叫函式(或者更確切地說是宏)import_spam()

PyMODINIT_FUNC
PyInit_client(void)
{
    PyObject *m;

    m = PyModule_Create(&clientmodule);
    if (m == NULL)
        return NULL;
    if (import_spam() < 0)
        return NULL;
    /* additional initialization can happen here */
    return m;
}

這種方法的主要缺點是檔案 spammodule.h 相當複雜。但是,每個匯出的函式的基本結構是相同的,因此只需要學習一次。

最後應該提到的是,Capsules 提供了額外的功能,這對於 Capsule 中儲存的指標的記憶體分配和釋放特別有用。詳細資訊在 Python/C API 參考手冊的 Capsules 部分以及 Capsules 的實現(Python 原始碼發行版中的檔案 Include/pycapsule.hObjects/pycapsule.c)中進行了描述。

腳註