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

如果您知道如何用 C 程式設計,那麼新增新的內建模組到 Python 中是相當容易的。這樣的 擴充套件模組 可以做兩件 Python 中無法直接完成的事情:它們可以實現新的內建物件型別,並且可以呼叫 C 庫函式和系統呼叫。

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

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

備註

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

1.1. 一個簡單的例子

讓我們建立一個名為 spam 的擴充套件模組(Monty Python 粉絲的最愛...),並且假設我們想建立一個 Python 介面來呼叫 C 庫函式 system() [1]。這個函式接受一個空終止的字元字串作為引數,並返回一個整數。我們希望可以像這樣從 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.h 包含多個標準標頭檔案。C 擴充套件應該包含它們使用的標準標頭檔案,並且不應依賴這些隱式包含。如果使用 Python 3.13 或更高版本的有限 C API,隱式包含的標頭檔案如下:

  • <assert.h>

  • <intrin.h>(在 Windows 上)

  • <inttypes.h>

  • <limits.h>

  • <math.h>

  • <stdarg.h>

  • <wchar.h>

  • <sys/types.h>(如果存在)

如果 Py_LIMITED_API 未定義,或者設定為版本 3.12 或更低版本,則還包含以下標頭檔案:

  • <ctype.h>

  • <unistd.h>(在 POSIX 系統上)

如果 Py_LIMITED_API 未定義,或者設定為版本 3.10 或更低版本,則還包含以下標頭檔案:

  • <errno.h>

  • <stdio.h>

  • <stdlib.h>

  • <string.h>

接下來,我們在模組檔案中新增將在 Python 表示式 spam.system(string) 被求值時呼叫的 C 函式(我們稍後會看到它將如何被呼叫)

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 函式總是有兩個引數,約定命名為 *self* 和 *args*。

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

args 引數將是指向包含引數的 Python 元組物件的指標。元組的每個項對應於呼叫引數列表中的一個引數。引數是 Python 物件 — 要在我們的 C 函式中對它們進行任何操作,我們必須將它們轉換為 C 值。Python API 中的函式 PyArg_ParseTuple() 檢查引數型別並將它們轉換為 C 值。它使用一個模板字串來確定引數所需的型別以及要將轉換後的值儲存到的 C 變數的型別。稍後會提供更多關於此的資訊。

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

1.2. 插曲:錯誤和異常

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

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

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

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

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

當函式 *f* 呼叫另一個函式 *g* 並檢測到後者失敗時,*f* 應該自己返回一個錯誤值(通常是 NULL 或 -1)。它不應該呼叫 PyErr_* 函式之一 — *g* 已經呼叫了一個。然後 *f* 的呼叫者也應該向 *its* 呼叫者返回一個錯誤指示,同樣呼叫 PyErr_*,依此類推 — 錯誤最詳細的原因已經由第一個檢測到它的函式報告。一旦錯誤到達 Python 直譯器的 `main` 迴圈,它就會中止當前正在執行的 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 = NULL;

並在模組的 Py_mod_exec 函式(spam_module_exec())中透過呼叫 PyErr_NewException() 來初始化它

SpamError = PyErr_NewException("spam.error", NULL, NULL);

由於 SpamError 是一個全域性變數,每次模組重新初始化時,當呼叫 Py_mod_exec 函式時,它都會被覆蓋。

現在,我們先避免這個問題:我們將透過引發一個 ImportError 來阻止重複初始化

static PyObject *SpamError = NULL;

static int
spam_module_exec(PyObject *m)
{
    if (SpamError != NULL) {
        PyErr_SetString(PyExc_ImportError,
                        "cannot initialize spam module more than once");
        return -1;
    }
    SpamError = PyErr_NewException("spam.error", NULL, NULL);
    if (PyModule_AddObjectRef(m, "SpamError", SpamError) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot spam_module_slots[] = {
    {Py_mod_exec, spam_module_exec},
    {0, NULL}
};

static struct PyModuleDef spam_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    .m_size = 0,  // non-negative
    .m_slots = spam_module_slots,
};

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}

請注意,異常物件的 Python 名稱是 spam.errorPyErr_NewException() 函式可以建立一個基類為 Exception 的類(除非傳遞了 NULL 而不是其他類),這在 內建異常 中有描述。

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

目前,用於刪除此引用的 Py_DECREF() 呼叫缺失。即使 Python 直譯器關閉,全域性 SpamError 變數也不會被垃圾回收。它會“洩露”。但是,我們確實確保了這種情況最多隻會發生一次。

我們將在本示例的後面討論 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 spam_methods[] = {
    ...
    {"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 spam_module = {
    ...
    .m_methods = spam_methods,
    ...
};

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

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}

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

PyInit_spam() 在每個直譯器首次匯入其模組 spam 時被呼叫。(有關嵌入 Python 的註釋,請參見下文。)必須透過 PyModuleDef_Init() 返回模組定義指標,以便匯入機制可以建立模組並將其儲存在 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 中刪除條目或在程序中的多個直譯器中匯入編譯的模組(或在 fork() 之後但未執行 exec())。如果模組狀態尚未完全 隔離,作者應考慮將模組標記為不支援子直譯器(透過 Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED)。

Python 原始碼分發版中包含一個更實質性的示例模組 Modules/xxlimited.c。該檔案可用作模板或僅作為示例閱讀。

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 keywdarg_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "keywdarg",
    .m_size = 0,
    .m_methods = keywdarg_methods,
};

PyMODINIT_FUNC
PyInit_keywdarg(void)
{
    return PyModuleDef_Init(&keywdarg_module);
}

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()。忘記處理已擁有的引用會造成記憶體洩漏。

也可以借用物件的引用。引用借用者不應呼叫 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() 及其同類函式不接管所有權 — 它們是“正常的”。)

當 C 函式從 Python 呼叫時,它會從呼叫者那裡借用對其引數的引用。呼叫者擁有對物件的引用,因此借用引用的生命週期保證直到函式返回。只有當必須儲存或傳遞此類借用的引用時,才必須透過呼叫 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] 的引用,然後用值 0 替換 list[1],最後列印借用的引用。看起來沒問題,對吧?但事實並非如此!

讓我們跟隨控制流進入 PyList_SetItem()。列表擁有其所有項的引用,因此當替換項 1 時,它必須處理原始項 1。現在假設原始項 1 是使用者定義類的例項,並且假設該類定義了一個 __del__() 方法。如果該類例項的引用計數為 1,則處理它會呼叫其 __del__() 方法。在內部,PyList_SetItem() 呼叫已替換項的 Py_DECREF(),這會呼叫已替換項對應的 tp_dealloc 函式。在取消分配期間,tp_dealloc 呼叫 tp_finalize,對於類例項,它對映到 __del__() 方法(請參閱 PEP 442)。整個序列發生在 PyList_SetItem() 呼叫期間。

由於 __del__() 方法是用 Python 編寫的,它可以執行任意 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*)。Capsules 只能透過其 C API 建立和訪問,但它們可以像任何其他 Python 物件一樣傳遞。特別是,它們可以分配給擴充套件模組名稱空間中的一個名稱。其他擴充套件模組然後可以匯入此模組,檢索此名稱的值,然後從 Capsule 中檢索指標。

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

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

特別是,用於暴露 C API 的 Capsules 應遵循以下約定命名:

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 用於告訴標頭檔案它正在被包含在匯出模組中,而不是客戶端模組。最後,模組的 mod_exec 函式必須負責初始化 C API 指標陣列

static int
spam_module_exec(PyObject *m)
{
    static void *PySpam_API[PySpam_API_pointers];
    PyObject *c_api_object;

    /* 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) {
        return -1;
    }

    return 0;
}

請注意,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() 所需要做的就是在其 mod_exec 函式中呼叫函式(或者更確切地說,宏) import_spam()

static int
client_module_exec(PyObject *m)
{
    if (import_spam() < 0) {
        return -1;
    }
    /* additional initialization can happen here */
    return 0;
}

這種方法的最大缺點是 spammodule.h 檔案相當複雜。但是,對於每個匯出的函式,基本結構都是相同的,所以只需要學習一次。

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

腳註