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 PyModuleDef custommodule = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

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

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

    return m;
}

現在,一次性接受這麼多有點多,但希望您能從上一章中熟悉一些內容。這個檔案定義了三件事

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

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

  3. 如何初始化 custom 模組:這是 PyInit_custom 函式和相關的 custommodule 結構體。

第一部分是

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,

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

if (PyType_Ready(&CustomType) < 0)
    return;

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

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

這會將型別新增到模組字典中。這使我們可以透過呼叫 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(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) 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 = 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;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    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(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    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", (PyCFunction) 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 = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
};

static PyModuleDef custommodule = {
    .m_base =PyModuleDef_HEAD_INIT,
    .m_name = "custom2",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom2(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

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

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

    return m;
}

這個版本的模組有一些變化。

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(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) self);
}

該方法被賦值給 tp_dealloc 成員。

.tp_dealloc = (destructor) Custom_dealloc,

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

注意

上面顯式轉換為 destructor 是必需的,因為我們將 Custom_dealloc 定義為接受一個 CustomObject * 引數,但是 tp_dealloc 函式指標期望接收一個 PyObject * 引數。否則,編譯器會發出警告。這是 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),您必須不要嘗試在執行時使用方法解析順序來確定要呼叫的方法。始終靜態地確定您要呼叫的型別,並直接呼叫其 tp_new,或透過 type->tp_base->tp_new 呼叫。如果您不這樣做,您型別的 Python 子類(也繼承自其他 Python 定義的類)可能無法正常工作。(具體來說,您可能無法建立此類子類的例項,而不會收到 TypeError。)

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

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    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 = (initproc) 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] 既不會釋放 GIL,也不會導致任何回撥到我們型別程式碼的呼叫時;

  • 當在不支援迴圈垃圾回收的型別上的 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(CustomObject *self, PyObject *Py_UNUSED(ignored))
{
    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", (PyCFunction) 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(CustomObject *self)
{
    Py_XDECREF(self->first);
    Py_XDECREF(self->last);
    Py_TYPE(self)->tp_free((PyObject *) 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 = 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;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    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(CustomObject *self, void *closure)
{
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    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(CustomObject *self, void *closure)
{
    return Py_NewRef(self->last);
}

static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    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", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

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

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) 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 = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static PyModuleDef custommodule = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom3",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom3(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

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

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

    return m;
}

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

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

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    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", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) 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(CustomObject *self, PyObject *args, PyObject *kwds)
{
    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(CustomObject *self, visitproc visit, void *arg)
{
    Py_VISIT(self->first);
    Py_VISIT(self->last);
    return 0;
}

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

static void
Custom_dealloc(CustomObject *self)
{
    PyObject_GC_UnTrack(self);
    Custom_clear(self);
    Py_TYPE(self)->tp_free((PyObject *) 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 = 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;
}

static int
Custom_init(CustomObject *self, PyObject *args, PyObject *kwds)
{
    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(CustomObject *self, void *closure)
{
    return Py_NewRef(self->first);
}

static int
Custom_setfirst(CustomObject *self, PyObject *value, void *closure)
{
    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(CustomObject *self, void *closure)
{
    return Py_NewRef(self->last);
}

static int
Custom_setlast(CustomObject *self, PyObject *value, void *closure)
{
    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", (getter) Custom_getfirst, (setter) Custom_setfirst,
     "first name", NULL},
    {"last", (getter) Custom_getlast, (setter) Custom_setlast,
     "last name", NULL},
    {NULL}  /* Sentinel */
};

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

static PyMethodDef Custom_methods[] = {
    {"name", (PyCFunction) 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 = (initproc) Custom_init,
    .tp_dealloc = (destructor) Custom_dealloc,
    .tp_traverse = (traverseproc) Custom_traverse,
    .tp_clear = (inquiry) Custom_clear,
    .tp_members = Custom_members,
    .tp_methods = Custom_methods,
    .tp_getset = Custom_getsetters,
};

static PyModuleDef custommodule = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "custom4",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_custom4(void)
{
    PyObject *m;
    if (PyType_Ready(&CustomType) < 0)
        return NULL;

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

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

    return m;
}

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

static int
Custom_traverse(CustomObject *self, visitproc visit, void *arg)
{
    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() 宏,用於自動呼叫 visit 函式。透過 Py_VISIT(),我們可以最大限度地減少 Custom_traverse 中的樣板程式碼。

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

注意

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

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

static int
Custom_clear(CustomObject *self)
{
    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 可能會呼叫任意程式碼。這意味著可以在函式內部觸發迴圈垃圾回收。由於垃圾回收機制假設引用計數不為零,因此我們需要在清除成員之前,透過呼叫 PyObject_GC_UnTrack() 從垃圾回收機制中取消對物件的跟蹤。以下是使用 PyObject_GC_UnTrack()Custom_clear 重新實現的析構器:

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

最後,我們將 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(SubListObject *self, PyObject *unused)
{
    self->state++;
    return PyLong_FromLong(self->state);
}

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

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

static PyTypeObject SubListType = {
    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 = (initproc) SubList_init,
    .tp_methods = SubList_methods,
};

static PyModuleDef sublistmodule = {
    PyModuleDef_HEAD_INIT,
    .m_name = "sublist",
    .m_doc = "Example module that creates an extension type.",
    .m_size = -1,
};

PyMODINIT_FUNC
PyInit_sublist(void)
{
    PyObject *m;
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0)
        return NULL;

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

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

    return m;
}

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

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

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

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

static int
SubList_init(SubListObject *self, PyObject *args, PyObject *kwds)
{
    if (PyList_Type.tp_init((PyObject *) self, 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 的引用來填充該欄位;它應該在模組初始化函式中稍後完成。

PyMODINIT_FUNC
PyInit_sublist(void)
{
    PyObject* m;
    SubListType.tp_base = &PyList_Type;
    if (PyType_Ready(&SubListType) < 0)
        return NULL;

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

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

    return m;
}

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

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

腳註