Using the C API: Assorted topics

The tutorial walked you through creating a C API extension module, but left many areas unexplained. This document looks at several concepts that you'll need to learn in order to write more complex extensions.

錯誤和例外

在整個 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,  // 非負數
    .m_slots = spam_module_slots,
};

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

請注意,例外物件的 Python 名稱是 spam.error。如同內建的例外所述,PyErr_NewException() 函式可能會建立一個基底類別為 Exception 的類別(除非傳入另一個類別來代替 NULL)。

請注意,SpamError 變數保留了對新建立的例外類別的參照;這是故意的!因為外部程式碼可能會從模組中移除這個例外,所以需要一個對這個類別的參照來確保它不會被丟棄而導致 SpamError 變成一個迷途指標 (dangling pointer)。如果它變成迷途指標,那產生例外的 C 程式碼可能會導致核心轉儲 (core dump) 或其他不預期的 side effect。

目前,用來移除此參照的 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);
}

Embedding an extension

如果你想讓你的模組成為 Python 直譯器的永久部分,你就必須更改組態設定並重新建置直譯器。在 Unix 上,將你的檔案(例如 spammodule.c)放在已解壓原始碼發行版本的 Modules/ 目錄中,在 Modules/Setup.local 檔案中新增描述你的檔案的一列:

spam spammodule.o

然後在最頂層目錄中執行 make 來重新建置直譯器。你也可以在 Modules/ 子目錄中執行 make,但你必須先在那裡執行「make Makefile」來重新建置 Makefile。(每次你修改 Setup 檔案時都需要這樣做。)

如果你的模組需要與額外的函式庫連結,這些也可以列在組態檔案中的該列上,例如:

spam spammodule.o -lX11

從 C 呼叫 Python 函式

前面的教學著重於讓 C 函式可以從 Python 呼叫。反過來也很有用:從 C 呼叫 Python 函式。對於支援所謂「回呼 (callback)」函式的函式庫尤其如此。如果一個 C 介面使用了回呼,等價的 Python 通常就需要為 Python 程式設計者提供一個回呼機制;其實作將需要從 C 回呼中呼叫 Python 回呼函式。其他用途也是可以想像的。

幸運的是,Python 直譯器可以很容易地被遞迴呼叫,並且有一個標準介面可以呼叫 Python 函式。(如果你有興趣了解如何以特定字串作為輸入來呼叫 Python 剖析器,請參閱 The Very High Level Layer。)

呼叫 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);         /* 為新的回呼增加一個參照 */
        Py_XDECREF(my_callback);  /* 釋放前一個回呼 */
        my_callback = temp;       /* 記住新的回呼 */
        /* 回傳 "None" 的樣板程式碼 */
        Py_INCREF(Py_None);
        result = Py_None;
    }
    return result;
}

這個函式必須在 PyMethodDef.ml_flags 中使用 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;
...
/* 是時候呼叫回呼了 */
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; /* 將錯誤傳回 */
...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; /* 將錯誤傳回 */
/* 這裡可以使用 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; /* 將錯誤傳回 */
/* 這裡可以使用 result */
Py_DECREF(result);

擴充函式中的參數提取

The tutorial uses a "METH_O" function, which is limited to a single Python argument. If you want more, you can use METH_VARARGS instead. With this flag, the C function will receive a tuple of arguments instead of a single object.

為了拆解 tuple,CPython 提供了 PyArg_ParseTuple() 函式,其宣告如下:

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

arg 引數必須是一個元組物件,包含從 Python 傳遞給 C 函式的引數串列。format 引數必須是一個格式字串,其語法在 Python/C API 參考手冊中的剖析引數與建置數值有說明。其餘引數必須是變數的位址,其型別由格式字串決定。

For example, to receive a single Python str object and turn it into a C buffer, you would use "s" as the format string:

const char *command;
if (!PyArg_ParseTuple(args, "s", &command)) {
    return NULL;
}

If an error is detected in the argument list, PyArg_ParseTuple() returns NULL (the error indicator for functions returning object pointers); your function may return NULL, relying on the exception set by PyArg_ParseTuple().

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

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

一些呼叫範例:

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

ok = PyArg_ParseTuple(args, ""); /* 沒有引數 */
    /* Python 呼叫:f() */
ok = PyArg_ParseTuple(args, "s", &s); /* 一個字串 */
    /* 可能的 Python 呼叫:f('whoops!') */
ok = PyArg_ParseTuple(args, "lls", &k, &l, &s); /* 兩個 long 和一個字串 */
    /* 可能的 Python 呼叫:f(1, 2, 'three') */
ok = PyArg_ParseTuple(args, "(ii)s#", &i, &j, &s, &size);
    /* 一對 int 和一個字串,其大小也會被回傳 */
    /* 可能的 Python 呼叫:f((1, 2), 'three') */
{
    const char *file;
    const char *mode = "r";
    int bufsize = 0;
    ok = PyArg_ParseTuple(args, "s|si", &file, &mode, &bufsize);
    /* 一個字串,以及可選的另一個字串和一個整數 */
    /* 可能的 Python 呼叫:
       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);
    /* 一個矩形和一個點 */
    /* 可能的 Python 呼叫:
       f(((0, 0), (400, 300)), (10, 10)) */
}
{
    Py_complex c;
    ok = PyArg_ParseTuple(args, "D:myfunction", &c);
    /* 一個複數,同時也提供函式名稱以便產生錯誤訊息 */
    /* 可能的 Python 呼叫:myfunction(1+2j) */
}

擴充函式的關鍵字參數

If you also want your function to accept keyword arguments, use the METH_KEYWORDS flag in combination with METH_VARARGS. (METH_KEYWORDS can also be used with other flags; see its documentation for the allowed combinations.)

在這種情況下,C 函式應該要能接受第三個 PyObject * 參數,這個參數將會是關鍵字的字典。可使用 PyArg_ParseTupleAndKeywords() 來剖析這種函式的引數。

PyArg_ParseTupleAndKeywords() 函式的宣告如下:

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

arg 和 format 參數與 PyArg_ParseTuple() 函式的相同。kwdict 參數是從 Python runtime 接收到的第三個參數,即關鍵字的字典。kwlist 參數是一個以 NULL 終止的字串 list,用於識別各參數;這些名稱會從左到右與 format 中的型別資訊進行配對。成功時,PyArg_ParseTupleAndKeywords() 會回傳 true,否則回傳 false 並引發適當的例外。

備註

使用關鍵字引數時無法剖析巢狀的 tuple!傳入的關鍵字參數如果不在 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[] = {
    /* 函式的轉型是必要的,因為 PyCFunction 值
     * 只接受兩個 PyObject* 參數,而 keywdarg_parrot() 接受
     * 三個。
     */
    {"parrot", (PyCFunction)(void(*)(void))keywdarg_parrot, METH_VARARGS | METH_KEYWORDS,
     "Print a lovely skit to standard output."},
    {NULL, NULL, 0, NULL}   /* 哨兵 */
};

建構任意值

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

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

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

與 PyArg_ParseTuple() 的一個區別是:後者要求它的第一個引數是一個 tuple(因為 Python 引數 list 在內部總是以 tuple 來表示),而 Py_BuildValue() 並不總是建構一個 tuple。只有當格式字串包含兩個或更多格式單元時,它才會建構一個 tuple。如果格式字串為空,它會回傳 None;如果格式字串恰好包含一個格式單元,它會回傳該格式單元所描述的任何物件。要強制它回傳大小為 0 或 1 的 tuple,請將格式字串加上括號。

範例(左邊是呼叫,右邊是結果的 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))

參照計數

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

每個使用 malloc() 配置的記憶體區塊,最終都應該透過恰好一次的 free() 呼叫回歸到可用記憶體池中。在正確的時機呼叫 free() 很重要。如果一個區塊的位址被遺忘了但沒有對它呼叫 free(),那麼它佔用的記憶體在程式終止之前都無法被重新使用。這稱為記憶體洩漏 (memory leak)。另一方面,如果程式對一個區塊呼叫了 free() 之後又繼續使用該區塊,這會與透過另一個 malloc() 呼叫重新使用該區塊產生衝突。這稱為使用已釋放的記憶體 (using freed memory)。它的後果和引用未初始化的資料一樣糟糕 --- core dump、錯誤的結果、神秘的當機。

記憶體洩漏的常見原因是程式碼中不尋常的路徑。例如,一個函式可能配置一塊記憶體、做一些計算,然後再次釋放該區塊。現在,對函式需求的變更可能會在計算中加入一個測試來偵測錯誤條件,並可以從函式中提前回傳。當走這條提前退出的路徑時,很容易忘記釋放已配置的記憶體區塊,特別是當它是後來才加入程式碼中的。這種洩漏一旦引入,通常會在很長一段時間內都不被發現:錯誤退出只在所有呼叫中的一小部分發生,而且大多數現代機器有充足的虛擬記憶體,所以洩漏只有在頻繁使用有洩漏函式的長時間執行程序中才會變得明顯。因此,透過採用能最小化這類錯誤的編碼慣例或策略來防止洩漏發生是很重要的。

由於 Python 大量使用 malloc() 和 free(),它需要一個策略來避免記憶體洩漏以及使用已釋放記憶體的問題。所選擇的方法稱為參照計數 (reference counting)。原理很簡單:每個物件包含一個計數器,當一個指向該物件的參照被儲存在某處時計數器遞增,當一個指向它的參照被刪除時計數器遞減。當計數器歸零時,表示該物件的最後一個參照已被刪除,物件便被釋放。

另一種替代策略稱為自動垃圾回收 (automatic garbage collection)。(有時,參照計數也被稱為一種垃圾回收策略,因此使用「自動」來區分兩者。)自動垃圾回收的最大優點是使用者不需要明確地呼叫 free()。(另一個聲稱的優點是速度或記憶體使用量的改善 --- 但這並非確定的事實。)缺點是對於 C 語言而言,並沒有真正可移植的自動垃圾回收器,而參照計數可以被可移植地實作(只要 malloc() 和 free() 函式可用 --- 這是 C 標準所保證的)。也許有一天會有足夠可移植的 C 自動垃圾回收器可用。在那之前,我們只能與參照計數共存。

雖然 Python 使用傳統的參照計數實作,但它也提供了一個能偵測參照循環的循環偵測器。這讓應用程式不必擔心建立直接或間接的循環參照;這些是僅使用參照計數來實作垃圾回收的弱點。參照循環由包含指向自身的(可能是間接的)參照的物件組成,使得循環中的每個物件的參照計數都不為零。典型的參照計數實作無法回收屬於參照循環中的任何物件的記憶體,或從循環中的物件所參照的記憶體,即使除了循環本身之外已經沒有其他的參照了。

循環偵測器能夠偵測垃圾循環並回收它們。gc 模組公開了一種執行偵測器的方式(collect() 函式),以及配置介面和在 runtime 停用偵測器的功能。

Python 中的參照計數

有兩個巨集,Py_INCREF(x) 和 Py_DECREF(x),用於處理參照計數的遞增和遞減。Py_DECREF() 也會在計數歸零時釋放物件。為了彈性,它不會直接呼叫 free() --- 而是透過物件的型別物件 (type object) 中的函式指標進行呼叫。為此目的(以及其他目的),每個物件也包含一個指向其型別物件的指標。

現在剩下的大問題是:何時使用 Py_INCREF(x) 和 Py_DECREF(x)?讓我們先介紹一些術語。沒有人「擁有」一個物件;然而,你可以擁有一個參照 (own a reference) 到一個物件。一個物件的參照計數現在被定義為指向它的被擁有參照的數量。參照的擁有者負責在不再需要該參照時呼叫 Py_DECREF()。參照的擁有權可以被轉移。處置一個被擁有參照有三種方式:傳遞它、儲存它、或呼叫 Py_DECREF()。忘記處置一個被擁有的參照會造成記憶體洩漏。

也可以借用 (borrow) [1] 一個物件的參照。借用參照的人不應呼叫 Py_DECREF()。借用者持有物件的時間不能超過借出參照的擁有者。在擁有者已經處置了參照之後使用借用參照,會有使用已釋放記憶體的風險,應該完全避免 [2]。

借用參照相比擁有參照的優點是,你不需要在程式碼中所有可能的路徑上都處理參照的處置 --- 換句話說,使用借用參照時,你不會在提前退出時有洩漏的風險。借用相比擁有的缺點是,在一些微妙的情況下,看似正確的程式碼中的借用參照可能在借出它的擁有者實際上已經處置了它之後被使用。

借用參照可以透過呼叫 Py_INCREF() 變成擁有的參照。這不會影響借出參照的擁有者的狀態 --- 它建立了一個新的擁有參照,並賦予完整的擁有者責任(新的擁有者必須適當地處置參照,就像先前的擁有者一樣)。

擁有權規則

每當一個物件參照被傳入或傳出一個函式時,擁有權是否隨參照一起轉移是該函式介面規格的一部分。

大多數回傳物件參照的函式會連同參照一起傳遞擁有權。特別是,所有用於建立新物件的函式,例如 PyLong_FromLong() 和 Py_BuildValue(),都會將擁有權傳遞給接收者。即使物件實際上不是新的,你仍然會收到該物件的新參照的擁有權。例如,PyLong_FromLong() 維護了一個常用值的快取,並可以回傳指向已快取項目的參照。

許多從其他物件中提取物件的函式也會隨著參照轉移擁有權,例如 PyObject_GetAttrString()。然而,這裡的情況不太明確,因為有一些常見的例行程式是例外:PyTuple_GetItem()、PyList_GetItem()、PyDict_GetItem() 和 PyDict_GetItemString() 都回傳你從 tuple、list 或 dictionary 中借用的參照。

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

當你將一個物件參照傳入另一個函式時,一般來說,該函式會向你借用參照 --- 如果它需要儲存它,它會使用 Py_INCREF() 來成為獨立的擁有者。這個規則恰好有兩個重要的例外:PyTuple_SetItem() 和 PyList_SetItem()。這些函式會接管傳入項目的擁有權 --- 即使它們失敗了也是!(注意 PyDict_SetItem() 和其相關函式不會接管擁有權 --- 它們是「正常的」。)

當一個 C 函式被 Python 呼叫時,它會從呼叫者借用其引數的參照。呼叫者擁有該物件的參照,所以借用參照的生命週期在函式回傳之前都是有保證的。只有當這種借用參照必須被儲存或傳遞時,才必須透過呼叫 Py_INCREF() 將它轉換為擁有的參照。

從被 Python 呼叫的 C 函式回傳的物件參照必須是擁有的參照 --- 擁有權從函式轉移給它的呼叫者。

薄冰

有一些情況下,看似無害的借用參照使用可能會導致問題。這些都與直譯器的隱式叫用有關,它可能導致參照的擁有者處置掉該參照。

需要知道的第一個也是最重要的情況是,在借用 list 項目的參照時,對一個不相關的物件使用 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() 的控制流程。list 擁有其所有項目的參照,所以當項目 1 被替換時,它必須處置原來的項目 1。現在假設原來的項目 1 是一個使用者定義類別的實例,並且進一步假設該類別定義了一個 __del__() 方法。如果這個類別實例的參照計數為 1,處置它將會呼叫其 __del__() 方法。在內部,PyList_SetItem() 會對被替換的項目呼叫 Py_DECREF(),這會叫用被替換項目對應的 tp_dealloc 函式。在解除配置期間,tp_dealloc 會呼叫 tp_finalize,它對於類別實例被對映到 __del__() 方法(見 PEP 442)。整個序列在 PyList_SetItem() 呼叫中同步發生。

由於它是用 Python 撰寫的,__del__() 方法可以執行任意的 Python 程式碼。它是否有可能做一些事情來使 bug() 中對 item 的參照失效?當然可以!假設傳入 bug() 的 list 對 __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! */
}

NULL 指標

一般而言,接受物件參照作為引數的函式不會預期你傳入 NULL 指標,如果你這樣做會導致核心傾印(core dump)(或導致之後的核心傾印)。回傳物件參照的函式通常只在有例外發生時才回傳 NULL。不對 NULL 引數進行測試的原因是,函式經常將它們收到的物件傳遞給其他函式──如果每個函式都測試 NULL,就會有大量多餘的測試,而且程式碼會執行得更慢。

比較好的做法是只在「源頭」測試 NULL:當收到可能為 NULL 的指標時,例如從 malloc() 或從可能引發例外的函式收到時。

巨集 Py_INCREF() 和 Py_DECREF() 不會檢查 NULL 指標──然而,它們的變體 Py_XINCREF() 和 Py_XDECREF() 會。

用來檢查特定物件型別的巨集(Pytype_Check())不會檢查 NULL 指標──同樣地,有很多程式碼會連續呼叫數個這類巨集來測試一個物件是否符合各種不同的預期型別,這樣會產生多餘的測試。沒有帶 NULL 檢查的變體。

C 函式呼叫機制保證傳遞給 C 函式的引數列表(範例中的 args)永遠不會是 NULL──事實上它保證它始終是一個 tuple [3]。

讓一個 NULL 指標「逃逸」到 Python 使用者端是一個嚴重的錯誤。

以 C++ 撰寫擴充

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

為擴充模組提供 C API

許多擴充模組只是提供新的函式和型別供 Python 使用,但有時擴充模組中的程式碼也可能對其他擴充模組有用。例如,一個擴充模組可以實作一種「collection」型別,它的運作方式類似無序的 list。就像標準的 Python list 型別有一個 C API 允許擴充模組建立和操作 list 一樣,這個新的 collection 型別也應該有一組 C 函式供其他擴充模組直接操作。

乍看之下這似乎很容易:只要撰寫函式(當然不要宣告為 static),提供適當的標頭檔,並撰寫 C API 的文件。事實上,如果所有擴充模組都是靜態連結到 Python 直譯器的,這樣做是行得通的。然而,當模組作為共享函式庫使用時,在一個模組中定義的符號可能對另一個模組不可見。可見性的細節取決於作業系統;有些系統對 Python 直譯器和所有擴充模組使用同一個全域命名空間(例如 Windows),而其他系統則要求在模組連結時明確列出匯入的符號(AIX 就是一個例子),或者提供不同策略的選擇(大多數 Unix 系統)。即使符號是全域可見的,想要呼叫其函式的模組也可能尚未被載入!

因此,可攜性要求不對符號可見性做任何假設。這意味著擴充模組中的所有符號都應該宣告為 static,模組的初始化函式除外,以避免與其他擴充模組的名稱衝突。而且這意味著應該從其他擴充模組存取的符號必須以不同的方式匯出。

Python 提供了一種特殊機制,用來在不同擴充模組之間傳遞 C 層級的資訊(指標):Capsule。Capsule 是一種 Python 資料型別,它儲存一個指標(void*)。Capsule 只能透過其 C API 來建立和存取,但它們可以像任何其他 Python 物件一樣被傳遞。特別是,它們可以被指派給擴充模組命名空間中的一個名稱。其他擴充模組可以接著 import 這個模組、取得這個名稱的值,然後從 Capsule 中取得指標。

有許多方式可以使用 Capsule 來匯出擴充模組的 C API。每個函式可以擁有自己的 Capsule,或者所有 C API 指標可以儲存在一個陣列中,其位址發布在一個 Capsule 裡。儲存和取得指標的各種任務可以在提供程式碼的模組和用戶端模組之間以不同方式分配。

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

特別是,用於公開 C API 的 Capsule 應該依照此慣例來命名:

modulename.attributename

便利函式 PyCapsule_Import() 使得載入透過 Capsule 提供的 C API 變得容易,但前提是 Capsule 的名稱符合此慣例。這個行為讓 C API 使用者對於他們載入的 Capsule 包含正確的 C API 有高度的確定性。

以下範例展示了一種將大部分負擔放在匯出模組的撰寫者身上的方法,這對於常用的函式庫模組來說是適當的。它將所有 C API 指標(在範例中只有一個!)儲存在一個 void 指標陣列中,該陣列成為 Capsule 的值。與該模組對應的標頭檔提供了一個巨集,負責 import 模組並取得其 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;

    /* 初始化 C API 指標陣列 */
    PySpam_API[PySpam_System_NUM] = (void *)PySpam_System;

    /* 建立一個包含 API 指標陣列位址的 Capsule */
    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

/* spammodule 的標頭檔 */

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

/* C API 指標的總數 */
#define PySpam_API_pointers 1


#ifdef SPAM_MODULE
/* 此區段在編譯 spammodule.c 時使用 */

static PySpam_System_RETURN PySpam_System PySpam_System_PROTO;

#else
/* 此區段在使用 spammodule API 的模組中使用 */

static void **PySpam_API;

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

/* 錯誤時回傳 -1,成功時回傳 0。
 * PyCapsule_Import 會在發生錯誤時設定例外。
 */
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;
    }
    /* 額外的初始化可以在這裡進行 */
    return 0;
}

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

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

註腳