1. 以 C 或 C++ 擴充 Python

如果你會撰寫 C 程式語言,那要向 Python 新增內建模組就不困難。這種擴充模組 (extension modules) 可以做兩件在 Python 中無法直接完成的事:它們可以實作新的內建物件型別,並且可以呼叫 C 的函式庫函式和系統呼叫。

為了支援擴充,Python API (Application Programmers Interface) 定義了一組函式、巨集和變數,提供對 Python run-time 系統大部分面向的存取。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] 使用,這個函式接受一個以 null 終止的 (null-terminated) 字元字串做為引數,並回傳一個整數。我們希望這個函式可以在 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 擴充應該引入它們使用的標準標頭檔,而不應依賴這些隱式的引入。如果使用限定 C API 版本 3.13 或更新版本,隱式引入的有:

  • <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>

接下來我們要加入到模組檔案的是 C 函式,當 Python 運算式 spam.system(string) 要被求值 (evaluated) 時就會被呼叫(我們很快就會看到它最後是如何被呼叫的):

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 直譯器中的一個重要慣例為:當一個函式失敗時,它就應該設定一個例外條件,並回傳一個錯誤值(通常是 -1 或一個 NULL 指標)。例外資訊會儲存在直譯器執行緒狀態的三個成員中。如果沒有例外,它們就會是 NULL。否則,它們是由 sys.exc_info() 所回傳的 Python 元組中的 C 等效元組。它們是例外型別、例外實例和回溯物件。了解它們對於理解錯誤是如何傳遞是很重要的。

Python API 定義了許多能夠設定各種類型例外的函式。

最常見的是 PyErr_SetString()。它的引數是一個例外物件和一個 C 字串。例外物件通常是預先定義的物件,例如 PyExc_ZeroDivisionError。C 字串則指出錯誤的原因,並被轉換為 Python 字串物件且被儲存為例外的「關聯值 (associated value)」。

另一個有用的函式是 PyErr_SetFromErrno(),它只接受一個例外引數,並透過檢查全域變數 errno 來建立關聯值。最一般的函式是 PyErr_SetObject(),它接受兩個物件引數,即例外和它的關聯值。你不需要對傳給任何這些函式的物件呼叫 Py_INCREF()

你可以使用 PyErr_Occurred() 來不具破壞性地測試例外是否已被設定。這會回傳目前的例外物件,如果沒有例外發生則回傳 NULL。你通常不需要呼叫 PyErr_Occurred() 來查看函式呼叫是否發生錯誤,因為你應可從回傳值就得知。

當函式 f 呼叫另一個函式 g 時檢測到後者失敗,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() 及同系列函式的這些重要例外,回傳整數狀態的函式通常會回傳一個正值或 0 表示成功、回傳 -1 表示失敗,就像 Unix 系統呼叫一樣。

最後,在回傳錯誤指示器時要注意垃圾清理(透過對你已經建立的物件呼叫 Py_XDECREF()Py_DECREF())!

你完全可以自行選擇要產生的例外。有一些預先宣告的 C 物件會對應到所有內建的 Python 例外,例如 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,