2. 定義擴充套件型別:教程

Python 允許 C 擴充套件模組的編寫者定義可以在 Python 程式碼中操作的新型別,類似於內建的 strlist 型別。所有擴充套件型別的程式碼都遵循一個模式,但您需要了解一些細節才能開始。本文件是對該主題的入門介紹。

2.1. 基礎知識

CPython 執行時將所有 Python 物件視為型別為 PyObject* 的變數,它充當所有 Python 物件的“基型別”。PyObject 結構本身只包含物件的引用計數和指向物件“型別物件”的指標。這是關鍵所在;型別物件決定了當直譯器查詢物件的屬性、呼叫方法或將其乘以另一個物件時,會呼叫哪些 (C) 函式。這些 C 函式被稱為“型別方法”。

因此,如果您想定義一個新的擴充套件型別,您需要建立一個新的型別物件。

這種事情只能透過例子來解釋,所以這裡是一個最小但完整的模組,它在一個 C 擴充套件模組 custom 中定義了一個名為 Custom 的新型別。

備註

我們這裡展示的是定義 靜態 擴充套件型別的傳統方式。它應該足以滿足大多數用途。C API 還允許使用 PyType_FromSpec() 函式定義堆分配的擴充套件型別,本教程不涉及此函式。

#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyObject_HEAD
    /* Type-specific fields go here. */
} CustomObject;

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

static int
custom_module_exec(PyObject *m)
{
    if (PyType_Ready(&CustomType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot custom_module_slots[] = {
    {Py_mod_exec, custom_module_exec},
    // Just use this while using static types
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom(void)
{
    return PyModuleDef_Init(&custom_module);
}

現在一次性接受這麼多資訊有些困難,但希望其中一些部分對您來說會與前一章的內容相似。此檔案定義了三件事:

  1. 一個 Custom 物件 包含什麼:這是 CustomObject 結構體,為每個 Custom 例項分配一次。

  2. Custom 型別 如何行為:這是 CustomType 結構體,它定義了一組標誌和函式指標,直譯器在請求特定操作時會檢查這些標誌和函式指標。

  3. 如何定義和執行 custom 模組:這是 PyInit_custom 函式和用於定義模組的關聯 custom_module 結構體,以及用於設定全新模組物件的 custom_module_exec 函式。

第一部分是

typedef struct {
    PyObject_HEAD
} CustomObject;

這是一個 Custom 物件將包含的內容。PyObject_HEAD 是每個物件結構開頭強制性的,它定義了一個名為 ob_base 的欄位,型別為 PyObject,包含一個指向型別物件和引用計數的指標(可以使用宏 Py_TYPEPy_REFCNT 分別訪問它們)。使用宏的原因是為了抽象佈局並允許在除錯構建中新增額外欄位。

備註

PyObject_HEAD 宏後面沒有分號。請注意不要意外新增分號:有些編譯器會報錯。

當然,物件除了標準的 PyObject_HEAD 樣板之外,通常還會儲存額外的資料;例如,以下是標準 Python 浮點數的定義:

typedef struct {
    PyObject_HEAD
    double ob_fval;
} PyFloatObject;

第二部分是型別物件的定義。

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_new = PyType_GenericNew,
};

備註

我們建議如上所示使用 C99 風格的指定初始化器,以避免列出所有您不關心的 PyTypeObject 欄位,也避免關心欄位的宣告順序。

object.hPyTypeObject 的實際定義包含比上述定義更多的欄位。其餘欄位將由 C 編譯器填充零,通常的做法是不明確指定它們,除非您需要它們。

我們將逐個欄位地分析它

.ob_base = PyVarObject_HEAD_INIT(NULL, 0)

這一行是強制性的樣板程式碼,用於初始化上面提到的 ob_base 欄位。

.tp_name = "custom.Custom",

我們型別的名稱。這會出現在我們物件的預設文字表示中和一些錯誤訊息中,例如

>>> "" + custom.Custom()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "custom.Custom") to str

請注意,名稱是一個點分隔名稱,包含模組名稱和模組內型別的名稱。本例中模組為 custom,型別為 Custom,因此我們將型別名稱設定為 custom.Custom。使用真實的帶點匯入路徑對於使您的型別與 pydocpickle 模組相容非常重要。

.tp_basicsize = sizeof(CustomObject),
.tp_itemsize = 0,

這是為了讓 Python 知道在建立新的 Custom 例項時需要分配多少記憶體。tp_itemsize 僅用於可變大小的物件,否則應為零。

備註

如果您的型別可以從 Python 進行子類化,並且您的型別與其基型別具有相同的 tp_basicsize,則您可能會遇到多重繼承問題。您的型別的 Python 子類必須在其 __bases__ 中首先列出您的型別,否則它將無法呼叫您型別的 __new__() 方法而會報錯。您可以透過確保您的型別具有比其基型別更大的 tp_basicsize 值來避免此問題。大多數情況下,這都是成立的,因為您的基型別要麼是 object,要麼您將向基型別新增資料成員,從而增加其大小。

我們將類標誌設定為 Py_TPFLAGS_DEFAULT

.tp_flags = Py_TPFLAGS_DEFAULT,

所有型別都應在其標誌中包含此常量。它啟用了至少 Python 3.3 之前定義的所有成員。如果您需要更多成員,則需要對相應的標誌進行 OR 運算。

我們在 tp_doc 中提供了型別的文件字串。

.tp_doc = PyDoc_STR("Custom objects"),

為了啟用物件建立,我們必須提供一個 tp_new 處理器。這相當於 Python 方法 __new__(),但必須明確指定。在這種情況下,我們可以直接使用 API 函式 PyType_GenericNew() 提供的預設實現。

.tp_new = PyType_GenericNew,

檔案中其他所有內容都應該很熟悉,除了 custom_module_exec() 中的一些程式碼

if (PyType_Ready(&CustomType) < 0) {
    return -1;
}

這會初始化 Custom 型別,將多個成員填充為適當的預設值,包括我們最初設定為 NULLob_type

if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
    return -1;
}

這會將型別新增到模組字典中。這允許我們透過呼叫 Custom 類來建立 Custom 例項。

>>> import custom
>>> mycustom = custom.Custom()

就這樣!剩下的就是構建它;將上面的程式碼放入一個名為 custom.c 的檔案中,

[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "custom"
version = "1"

在一個名為 pyproject.toml 的檔案中,以及

from setuptools import Extension, setup
setup(ext_modules=[Extension("custom", ["custom.c"])])

在一個名為 setup.py 的檔案中;然後輸入

$ python -m pip install .

在 shell 中,應該會在子目錄中生成一個名為 custom.so 的檔案並安裝它;現在啟動 Python — 您應該能夠 import custom 並使用 Custom 物件。

這不是很困難,不是嗎?

當然,當前的 Custom 型別相當無趣。它沒有資料,也不做任何事情。甚至不能被子類化。

2.2. 在基本示例中新增資料和方法

讓我們擴充套件基本示例,新增一些資料和方法。我們還要使該型別可以用作基類。我們將建立一個新模組 custom2,它將新增這些功能。

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

static void
Custom_dealloc(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_XSETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_XSETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"first", Py_T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", Py_T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom2.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = Custom_init,
    .tp_dealloc = Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
};

static int
custom_module_exec(PyObject *m)
{
    if (PyType_Ready(&CustomType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot custom_module_slots[] = {
    {Py_mod_exec, custom_module_exec},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom2",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom2(void)
{
    return PyModuleDef_Init(&custom_module);
}

此版本的模組有許多更改。

Custom 型別現在在其 C 結構中包含三個資料屬性:firstlastnumberfirstlast 變數是包含名字和姓氏的 Python 字串。 number 屬性是一個 C 整數。

物件結構相應地更新

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

因為我們現在有資料需要管理,所以我們必須更加謹慎地處理物件的分配和釋放。至少,我們需要一個釋放方法:

static void
Custom_dealloc(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(self);
}

它被賦值給 tp_dealloc 成員

.tp_dealloc = Custom_dealloc,

此方法首先清除兩個 Python 屬性的引用計數。Py_XDECREF() 正確處理其引數為 NULL 的情況(如果 tp_new 中途失敗,則可能發生這種情況)。然後它呼叫物件型別(由 Py_TYPE(self) 計算)的 tp_free 成員來釋放物件的記憶體。請注意,物件的型別可能不是 CustomType,因為該物件可能是子類的例項。

備註

上述到 CustomObject * 的顯式型別轉換是必要的,因為我們定義 Custom_dealloc 接受 PyObject * 引數,正如 tp_dealloc 函式指標期望接收 PyObject * 引數一樣。透過賦值給型別的 tp_dealloc 槽,我們宣告它只能使用我們的 CustomObject 類的例項進行呼叫,因此轉換為 (CustomObject *) 是安全的。這就是 C 語言中的面向物件多型性!

在現有程式碼或本教程的早期版本中,您可能會看到類似的函式直接採用子型別物件結構 (CustomObject*) 的指標,如下所示:

Custom_dealloc(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}
...
.tp_dealloc = (destructor) Custom_dealloc,

這在 CPython 支援的所有架構上執行相同的操作,但根據 C 標準,它會呼叫未定義的行為。

我們希望確保名字和姓氏被初始化為空字串,因此我們提供一個 tp_new 實現。

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = PyUnicode_FromString("");
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = PyUnicode_FromString("");
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

並將其安裝到 tp_new 成員中

.tp_new = Custom_new,

tp_new 處理程式負責建立(而不是初始化)該型別的物件。它在 Python 中作為 __new__() 方法公開。它不需要定義 tp_new 成員,實際上許多擴充套件型別會簡單地重用 PyType_GenericNew(),就像上面 Custom 型別的第一個版本所做的那樣。在這種情況下,我們使用 tp_new 處理程式將 firstlast 屬性初始化為非 NULL 的預設值。

tp_new 接收被例項化的型別(如果例項化的是子類,則不一定是 CustomType)和呼叫該型別時傳遞的任何引數,並期望返回建立的例項。tp_new 處理程式總是接受位置引數和關鍵字引數,但它們通常會忽略這些引數,將引數處理留給初始化器(即 C 中的 tp_init 或 Python 中的 __init__)方法。

備註

tp_new 不應顯式呼叫 tp_init,因為直譯器會自行呼叫它。

tp_new 實現呼叫 tp_alloc 槽來分配記憶體

self = (CustomObject *) type->tp_alloc(type, 0);

由於記憶體分配可能會失敗,我們必須在繼續之前檢查 tp_alloc 結果是否為 NULL

備註

我們沒有自己填寫 tp_alloc 槽。而是 PyType_Ready() 透過從我們的基類(預設情況下是 object)繼承它來為我們填充它。大多數型別使用預設的分配策略。

備註

如果您正在建立一個協作式 tp_new(一個呼叫基類的 tp_new__new__() 的函式),則 絕不能 在執行時使用方法解析順序來確定要呼叫的方法。始終靜態地確定要呼叫的型別,並直接呼叫其 tp_new,或者透過 type->tp_base->tp_new 呼叫。如果您不這樣做,則您的型別的 Python 子類(也繼承自其他 Python 定義的類)可能無法正常工作。(具體來說,您可能無法建立此類子類的例項而不會遇到 TypeError。)

我們還定義了一個初始化函式,它接受引數來為我們的例項提供初始值。

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_XDECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_XDECREF(tmp);
    }
    return 0;
}

透過填充 tp_init 槽。

.tp_init = Custom_init,

tp_init 槽在 Python 中作為 __init__() 方法公開。它用於在物件建立後對其進行初始化。初始化器總是接受位置引數和關鍵字引數,並且在成功時應返回 0,在失敗時應返回 -1

tp_new 處理程式不同,不能保證 tp_init 會被呼叫(例如,pickle 模組預設不會在未解封的例項上呼叫 __init__())。它也可能被多次呼叫。任何人都可以對我們的物件呼叫 __init__() 方法。因此,在賦值新的屬性值時,我們必須格外小心。例如,我們可能會試圖這樣賦值 first 成員:

if (first) {
    Py_XDECREF(self->first);
    Py_INCREF(first);
    self->first = first;
}

但這將是危險的。我們的型別不限制 first 成員的型別,因此它可以是任何型別的物件。它可能有一個解構函式,導致執行試圖訪問 first 成員的程式碼;或者該解構函式可能會分離執行緒狀態,並允許任意程式碼在其他執行緒中執行,這些程式碼訪問和修改我們的物件。

為了安全起見,並保護我們自己免受這種可能性,我們幾乎總是在遞減引用計數之前重新分配成員。我們什麼時候不必這樣做?

  • 當我們絕對知道引用計數大於1時;

  • 當我們知道物件的釋放 [1] 既不會分離執行緒狀態,也不會導致對我們型別的程式碼的任何回撥;

  • 在不支援迴圈垃圾回收的型別的 tp_dealloc 處理器中遞減引用計數時 [2]

我們希望將我們的例項變數公開為屬性。有多種方法可以做到這一點。最簡單的方法是定義成員定義:

static PyMemberDef Custom_members[] = {
    {"first", Py_T_OBJECT_EX, offsetof(CustomObject, first), 0,
     "first name"},
    {"last", Py_T_OBJECT_EX, offsetof(CustomObject, last), 0,
     "last name"},
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

並將定義放入 tp_members 槽中

.tp_members = Custom_members,

每個成員定義都包含成員名稱、型別、偏移量、訪問標誌和文件字串。詳見下文的通用屬性管理部分。

這種方法的缺點是,它無法限制可以分配給 Python 屬性的物件的型別。我們期望名字和姓氏是字串,但可以分配任何 Python 物件。此外,屬性可以被刪除,將 C 指標設定為 NULL。儘管我們可以確保成員被初始化為非 NULL 值,但如果屬性被刪除,成員可以被設定為 NULL

我們定義了一個方法 Custom.name(),它將物件的名稱輸出為名字和姓氏的連線。

static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    if (self->first == NULL) {
        PyErr_SetString(PyExc_AttributeError, "first");
        return NULL;
    }
    if (self->last == NULL) {
        PyErr_SetString(PyExc_AttributeError, "last");
        return NULL;
    }
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

該方法作為 C 函式實現,它將 Custom(或 Custom 子類)例項作為第一個引數。方法總是將例項作為第一個引數。方法通常也接受位置引數和關鍵字引數,但在本例中我們不接受任何引數,也不需要接受位置引數元組或關鍵字引數字典。此方法等同於 Python 方法:

def name(self):
    return "%s %s" % (self.first, self.last)

請注意,我們必須檢查 firstlast 成員是否為 NULL。這是因為它們可以被刪除,在這種情況下它們被設定為 NULL。最好阻止這些屬性的刪除並將屬性值限制為字串。我們將在下一節中瞭解如何做到這一點。

現在我們已經定義了方法,我們需要建立一個方法定義陣列。

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

(注意,我們使用了 METH_NOARGS 標誌來表示該方法除了 self 之外不期望任何引數)

並將其分配給 tp_methods

.tp_methods = Custom_methods,

最後,我們將使我們的型別可用作子類化的基類。到目前為止,我們已仔細編寫了方法,使其不會對正在建立或使用的物件的型別做出任何假設,因此我們只需將 Py_TPFLAGS_BASETYPE 新增到我們的類標誌定義中

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,

我們將 PyInit_custom() 重新命名為 PyInit_custom2(),更新 PyModuleDef 結構中的模組名稱,並更新 PyTypeObject 結構中的完整類名稱。

最後,我們更新我們的 setup.py 檔案以包含新模組,

from setuptools import Extension, setup
setup(ext_modules=[
    Extension("custom", ["custom.c"]),
    Extension("custom2", ["custom2.c"]),
])

然後我們重新安裝,以便我們可以 import custom2

$ python -m pip install .

2.3. 提供對資料屬性的更精細控制

在本節中,我們將更精細地控制 Custom 示例中 firstlast 屬性的設定方式。在我們模組的先前版本中,例項變數 firstlast 可以設定為非字串值甚至被刪除。我們希望確保這些屬性始終包含字串。

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

static void
Custom_dealloc(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free(self);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_SETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_SETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    Py_SETREF(self->first, Py_NewRef(value));
    return 0;
}

static PyObject *
Custom_getlast(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->last);
}

static int
Custom_setlast(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    Py_SETREF(self->last, Py_NewRef(value));
    return 0;
}

static PyGetSetDef Custom_getsetters[] = {
    {"first", Custom_getfirst, Custom_setfirst,
     "first name", NULL},
    {"last", Custom_getlast, Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom3.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = Custom_new,
    .tp_init = Custom_init,
    .tp_dealloc = Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static int
custom_module_exec(PyObject *m)
{
    if (PyType_Ready(&CustomType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot custom_module_slots[] = {
    {Py_mod_exec, custom_module_exec},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom3",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom3(void)
{
    return PyModuleDef_Init(&custom_module);
}

為了對 firstlast 屬性提供更大的控制,我們將使用自定義 getter 和 setter 函式。以下是獲取和設定 first 屬性的函式:

static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    Py_INCREF(self->first);
    return self->first;
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    PyObject *tmp;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    tmp = self->first;
    Py_INCREF(value);
    self->first = value;
    Py_DECREF(tmp);
    return 0;
}

getter 函式被傳遞一個 Custom 物件和一個“閉包”,閉包是一個 void 指標。在這種情況下,閉包被忽略。(閉包支援一種高階用法,其中定義資料被傳遞給 getter 和 setter。例如,這可以用於允許一組 getter 和 setter 函式,它們根據閉包中的資料決定獲取或設定哪個屬性。)

setter 函式被傳遞 Custom 物件、新值和閉包。新值可能為 NULL,在這種情況下,該屬性將被刪除。在我們的 setter 中,如果屬性被刪除或其新值不是字串,我們將引發錯誤。

我們建立了一個 PyGetSetDef 結構體陣列

static PyGetSetDef Custom_getsetters[] = {
    {"first", Custom_getfirst, Custom_setfirst,
     "first name", NULL},
    {"last", Custom_getlast, Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

並將其註冊到 tp_getset 槽中

.tp_getset = Custom_getsetters,

PyGetSetDef 結構體中的最後一項是上面提到的“閉包”。在這種情況下,我們不使用閉包,因此我們只需傳遞 NULL

我們還刪除了這些屬性的成員定義

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

我們還需要更新 tp_init 處理器,只允許傳遞字串 [3]

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL, *tmp;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        tmp = self->first;
        Py_INCREF(first);
        self->first = first;
        Py_DECREF(tmp);
    }
    if (last) {
        tmp = self->last;
        Py_INCREF(last);
        self->last = last;
        Py_DECREF(tmp);
    }
    return 0;
}

透過這些更改,我們可以確保 firstlast 成員永遠不會是 NULL,因此我們幾乎在所有情況下都可以刪除對 NULL 值的檢查。這意味著大多數 Py_XDECREF() 呼叫可以轉換為 Py_DECREF() 呼叫。唯一不能更改這些呼叫的地方是在 tp_dealloc 實現中,因為這些成員在 tp_new 中初始化失敗的可能性。

我們還像以前一樣,重新命名了模組初始化函式和初始化函式中的模組名稱,並且在 setup.py 檔案中添加了一個額外的定義。

2.4. 支援迴圈垃圾回收

Python 有一個迴圈垃圾回收器 (GC),即使物件的引用計數不為零,它也能識別不需要的物件。當物件參與迴圈時,可能會發生這種情況。例如,考慮:

>>> l = []
>>> l.append(l)
>>> del l

在這個例子中,我們建立了一個包含自身的列表。當我們刪除它時,它仍然有來自自身的引用。它的引用計數不會降到零。幸運的是,Python 的迴圈垃圾回收器最終會發現該列表是垃圾並釋放它。

Custom 示例的第二個版本中,我們允許任何型別的物件儲存在 firstlast 屬性中 [4]。此外,在第二和第三個版本中,我們允許子類化 Custom,並且子類可以新增任意屬性。由於這兩個原因中的任何一個,Custom 物件都可以參與迴圈。

>>> import custom3
>>> class Derived(custom3.Custom): pass
...
>>> n = Derived()
>>> n.some_attribute = n

為了使參與引用迴圈的 Custom 例項能夠被迴圈 GC 正確檢測和回收,我們的 Custom 型別需要填充兩個額外的槽位並啟用一個標誌來啟用這些槽位。

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <stddef.h> /* for offsetof() */

typedef struct {
    PyObject_HEAD
    PyObject *first; /* first name */
    PyObject *last;  /* last name */
    int number;
} CustomObject;

static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
    CustomObject *self = (CustomObject *) op;
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

static int
Custom_clear(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}

static void
Custom_dealloc(PyObject *op)
{
    PyObject_GC_UnTrack(op);
    (void)Custom_clear(op);
    Py_TYPE(op)->tp_free(op);
}

static PyObject *
Custom_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    CustomObject *self;
    self = (CustomObject *) type->tp_alloc(type, 0);
    if (self != NULL) {
        self->first = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->first == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->last = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
        if (self->last == NULL) {
            Py_DECREF(self);
            return NULL;
        }
        self->number = 0;
    }
    return (PyObject *) self;
}

static int
Custom_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    CustomObject *self = (CustomObject *) op;
    static char *kwlist[] = {"first", "last", "number", NULL};
    PyObject *first = NULL, *last = NULL;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|UUi", kwlist,
                                     &first, &last,
                                     &self->number))
        return -1;

    if (first) {
        Py_SETREF(self->first, Py_NewRef(first));
    }
    if (last) {
        Py_SETREF(self->last, Py_NewRef(last));
    }
    return 0;
}

static PyMemberDef Custom_members[] = {
    {"number", Py_T_INT, offsetof(CustomObject, number), 0,
     "custom number"},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_getfirst(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the first attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The first attribute value must be a string");
        return -1;
    }
    Py_XSETREF(self->first, Py_NewRef(value));
    return 0;
}

static PyObject *
Custom_getlast(PyObject *op, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    return Py_NewRef(self->last);
}

static int
Custom_setlast(PyObject *op, PyObject *value, void *closure)
{
    CustomObject *self = (CustomObject *) op;
    if (value == NULL) {
        PyErr_SetString(PyExc_TypeError, "Cannot delete the last attribute");
        return -1;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "The last attribute value must be a string");
        return -1;
    }
    Py_XSETREF(self->last, Py_NewRef(value));
    return 0;
}

static PyGetSetDef Custom_getsetters[] = {
    {"first", Custom_getfirst, Custom_setfirst,
     "first name", NULL},
    {"last", Custom_getlast, Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

static PyObject *
Custom_name(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    CustomObject *self = (CustomObject *) op;
    return PyUnicode_FromFormat("%S %S", self->first, self->last);
}

static PyMethodDef Custom_methods[] = {
    {"name", Custom_name, METH_NOARGS,
     "Return the name, combining the first and last name"
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject CustomType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "custom4.Custom",
    .tp_doc = PyDoc_STR("Custom objects"),
    .tp_basicsize = sizeof(CustomObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
    .tp_new = Custom_new,
    .tp_init = Custom_init,
    .tp_dealloc = Custom_dealloc,
    .tp_traverse = Custom_traverse,
    .tp_clear = Custom_clear,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static int
custom_module_exec(PyObject *m)
{
    if (PyType_Ready(&CustomType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "Custom", (PyObject *) &CustomType) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot custom_module_slots[] = {
    {Py_mod_exec, custom_module_exec},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef custom_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom4",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = custom_module_slots,
};

PyMODINIT_FUNC
PyInit_custom4(void)
{
    return PyModuleDef_Init(&custom_module);
}

首先,遍歷方法讓迴圈 GC 知道可能參與迴圈的子物件

static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
    CustomObject *self = (CustomObject *) op;
    int vret;
    if (self->first) {
        vret = visit(self->first, arg);
        if (vret != 0)
            return vret;
    }
    if (self->last) {
        vret = visit(self->last, arg);
        if (vret != 0)
            return vret;
    }
    return 0;
}

對於每個可能參與迴圈的子物件,我們需要呼叫 visit() 函式,該函式被傳遞給遍歷方法。visit() 函式接受子物件和傳遞給遍歷方法的額外引數 arg 作為引數。它返回一個整數值,如果該值非零,則必須返回。

Python 提供了一個 Py_VISIT() 宏,它自動化呼叫訪問函式。使用 Py_VISIT(),我們可以最小化 Custom_traverse 中的樣板程式碼

static int
Custom_traverse(PyObject *op, visitproc visit, void *arg)
{
    CustomObject *self = (CustomObject *) op;
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

備註

tp_traverse 的實現必須將其引數準確命名為 visitarg 才能使用 Py_VISIT()

其次,我們需要提供一個清除任何可能參與迴圈的子物件的方法。

static int
Custom_clear(PyObject *op)
{
    CustomObject *self = (CustomObject *) op;
    Py_CLEAR(self->first);
    Py_CLEAR(self->last);
    return 0;
}

請注意 Py_CLEAR() 宏的使用。它是清除任意型別資料屬性同時遞減其引用計數的推薦且安全的方法。如果您在將屬性設定為 NULL 之前呼叫 Py_XDECREF(),則屬性的解構函式有可能回撥到再次讀取屬性的程式碼中(尤其是 存在引用迴圈時)。

備註

您可以這樣編寫程式碼來模擬 Py_CLEAR()

PyObject *tmp;
tmp = self->first;
self->first = NULL;
Py_XDECREF(tmp);

然而,在刪除屬性時始終使用 Py_CLEAR() 要容易得多,也更不容易出錯。不要為了微最佳化而犧牲健壯性!

解分配器 Custom_dealloc 在清除屬性時可能會呼叫任意程式碼。這意味著迴圈 GC 可以在函式內部觸發。由於 GC 假定引用計數不為零,我們需要透過呼叫 PyObject_GC_UnTrack() 在清除成員之前取消跟蹤 GC 中的物件。以下是我們使用 PyObject_GC_UnTrack()Custom_clear 重新實現的解分配器。

static void
Custom_dealloc(PyObject *op)
{
    PyObject_GC_UnTrack(op);
    (void)Custom_clear(op);
    Py_TYPE(op)->tp_free(op);
}

最後,我們將 Py_TPFLAGS_HAVE_GC 標誌新增到類標誌中

.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,

差不多就是這樣了。如果我們編寫了自定義的 tp_alloctp_free 處理程式,我們需要為迴圈垃圾回收修改它們。大多數擴充套件會使用自動提供的版本。

2.5. 子類化其他型別

可以建立從現有型別派生出來的新擴充套件型別。從內建型別繼承最容易,因為擴充套件可以輕鬆使用它需要的 PyTypeObject。在擴充套件模組之間共享這些 PyTypeObject 結構可能很困難。

在此示例中,我們將建立一個繼承自內建 list 型別的 SubList 型別。新型別將與普通列表完全相容,但會有一個額外的 increment() 方法,用於增加內部計數器。

>>> import sublist
>>> s = sublist.SubList(range(3))
>>> s.extend(s)
>>> print(len(s))
6
>>> print(s.increment())
1
>>> print(s.increment())
2
#define PY_SSIZE_T_CLEAN
#include <Python.h>

typedef struct {
    PyListObject list;
    int state;
} SubListObject;

static PyObject *
SubList_increment(PyObject *op, PyObject *Py_UNUSED(dummy))
{
    SubListObject *self = (SubListObject *) op;
    self->state++;
    return PyLong_FromLong(self->state);
}

static PyMethodDef SubList_methods[] = {
    {"increment", SubList_increment, METH_NOARGS,
     PyDoc_STR("increment state counter")},
    {NULL},
};

static int
SubList_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    SubListObject *self = (SubListObject *) op;
    if (PyList_Type.tp_init(op, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}

static PyTypeObject SubListType = {
    .ob_base = PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "sublist.SubList",
    .tp_doc = PyDoc_STR("SubList objects"),
    .tp_basicsize = sizeof(SubListObject),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_init = SubList_init,
    .tp_methods = SubList_methods,
};

static int
sublist_module_exec(PyObject *m)
{
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
        return -1;
    }

    return 0;
}

static PyModuleDef_Slot sublist_module_slots[] = {
    {Py_mod_exec, sublist_module_exec},
    {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED},
    {0, NULL}
};

static PyModuleDef sublist_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "sublist",
    .m_doc = "Example module that creates an extension type.",
    .m_size = 0,
    .m_slots = sublist_module_slots,
};

PyMODINIT_FUNC
PyInit_sublist(void)
{
    return PyModuleDef_Init(&sublist_module);
}

如您所見,原始碼與前面章節的 Custom 示例非常相似。我們將分解它們之間的主要區別。

typedef struct {
    PyListObject list;
    int state;
} SubListObject;

派生型別物件的主要區別在於基類物件結構必須是第一個值。基類在其結構開頭已經包含 PyObject_HEAD()

當一個 Python 物件是 SubList 例項時,它的 PyObject * 指標可以安全地轉換為 PyListObject *SubListObject *

static int
SubList_init(PyObject *op, PyObject *args, PyObject *kwds)
{
    SubListObject *self = (SubListObject *) op;
    if (PyList_Type.tp_init(op, args, kwds) < 0)
        return -1;
    self->state = 0;
    return 0;
}

上面我們看到了如何呼叫基類的 __init__() 方法。

在編寫具有自定義 tp_newtp_dealloc 成員的型別時,這種模式非常重要。tp_new 處理器不應實際使用其 tp_alloc 為物件建立記憶體,而是透過呼叫其自身的 tp_new 讓基類處理它。

PyTypeObject 結構體支援一個 tp_base 欄位,用於指定型別的具體基類。由於跨平臺編譯器問題,您不能直接用對 PyList_Type 的引用填充該欄位;這應該在 Py_mod_exec 函式中完成。

static int
sublist_module_exec(PyObject *m)
{
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0) {
        return -1;
    }

    if (PyModule_AddObjectRef(m, "SubList", (PyObject *) &SubListType) < 0) {
        return -1;
    }

    return 0;
}

在呼叫 PyType_Ready() 之前,型別結構必須填充 tp_base 槽。當我們派生現有型別時,不必用 PyType_GenericNew() 填充 tp_alloc 槽——分配函式將從基型別繼承。

之後,呼叫 PyType_Ready() 並將型別物件新增到模組與基本 Custom 示例相同。

腳註