3. 定義擴充套件型別:雜項

本節旨在快速概覽可以實現的各種型別方法及其作用。

這是 PyTypeObject 的定義,其中一些僅在除錯版本中使用的欄位已省略

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    reprfunc tp_repr;

    /* Method suites for standard classes */

    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

    /* More standard operations (here for binary compatibility) */

    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;

    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;

    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */

    /* Assigned meaning in release 2.0 */
    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* Assigned meaning in release 2.1 */
    /* rich comparisons */
    richcmpfunc tp_richcompare;

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Attribute descriptor and subclassing stuff */
    PyMethodDef *tp_methods;
    PyMemberDef *tp_members;
    PyGetSetDef *tp_getset;
    // Strong reference on a heap type, borrowed reference on a static type
    PyTypeObject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache; /* no longer used */
    void *tp_subclasses;  /* for static builtin types this is an index */
    PyObject *tp_weaklist; /* not used for static builtin types */
    destructor tp_del;

    /* Type attribute cache version tag. Added in version 2.6.
     * If zero, the cache is invalid and must be initialized.
     */
    unsigned int tp_version_tag;

    destructor tp_finalize;
    vectorcallfunc tp_vectorcall;

    /* bitset of which type-watchers care about this type */
    unsigned char tp_watched;

    /* Number of tp_version_tag values used.
     * Set to _Py_ATTR_CACHE_UNUSED if the attribute cache is
     * disabled for this type (e.g. due to custom MRO entries).
     * Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere).
     */
    uint16_t tp_versions_used;
} PyTypeObject;

這可真是一大堆方法。不過不必過於擔心——如果你想定義一個型別,你很可能只需要實現其中的少數幾個。

正如你可能已經預料到的,我們將對這些內容進行講解,並提供有關各種處理程式的更多資訊。我們不會按照它們在結構中定義的順序進行,因為有很多歷史包袱影響了欄位的順序。通常最簡單的方法是找到一個包含所需欄位的示例,然後更改值以適應你的新型別。

const char *tp_name; /* For printing */

型別的名稱——如上一章所述,這將出現在各種地方,幾乎完全用於診斷目的。儘量選擇在這種情況下有幫助的名稱!

Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

這些欄位告訴執行時在建立此型別的新物件時分配多少記憶體。Python 對可變長度結構(例如:字串、元組)有一些內建支援,這就是 tp_itemsize 欄位的來源。這將在稍後處理。

const char *tp_doc;

你可以在此處放置一個字串(或其地址),當 Python 指令碼引用 obj.__doc__ 來檢索文件字串時,該字串將返回。

現在我們來到基本型別方法——大多數擴充套件型別都將實現的方法。

3.1. 終結和解除分配

destructor tp_dealloc;

當你的型別例項的引用計數減少到零並且 Python 直譯器想要回收它時,將呼叫此函式。如果你的型別有記憶體要釋放或需要執行其他清理,你可以將其放在此處。物件本身也需要在此處釋放。以下是此函式的一個示例

static void
newdatatype_dealloc(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    free(self->obj_UnderlyingDatatypePtr);
    Py_TYPE(self)->tp_free(self);
}

如果你的型別支援垃圾回收,解構函式應在清除任何成員欄位之前呼叫 PyObject_GC_UnTrack()

static void
newdatatype_dealloc(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    PyObject_GC_UnTrack(op);
    Py_CLEAR(self->other_obj);
    ...
    Py_TYPE(self)->tp_free(self);
}

解構函式的一個重要要求是它不處理任何待處理的異常。這一點很重要,因為解構函式通常在直譯器解開 Python 棧時被呼叫;當棧因異常(而不是正常返回)而解開時,不會採取任何措施來保護解構函式免受已設定異常的影響。解構函式執行的任何可能導致額外 Python 程式碼執行的操作都可能檢測到已設定異常。這可能導致直譯器產生誤導性錯誤。防止這種情況的正確方法是在執行不安全操作之前儲存待處理的異常,並在完成後恢復它。這可以使用 PyErr_Fetch()PyErr_Restore() 函式來完成

static void
my_dealloc(PyObject *obj)
{
    MyObject *self = (MyObject *) obj;
    PyObject *cbresult;

    if (self->my_callback != NULL) {
        PyObject *err_type, *err_value, *err_traceback;

        /* This saves the current exception state */
        PyErr_Fetch(&err_type, &err_value, &err_traceback);

        cbresult = PyObject_CallNoArgs(self->my_callback);
        if (cbresult == NULL) {
           PyErr_WriteUnraisable(self->my_callback);
        }
        else {
            Py_DECREF(cbresult);
        }

        /* This restores the saved exception state */
        PyErr_Restore(err_type, err_value, err_traceback);

        Py_DECREF(self->my_callback);
    }
    Py_TYPE(self)->tp_free(self);
}

備註

在解構函式中可以安全執行的操作是有限制的。首先,如果你的型別支援垃圾回收(使用 tp_traverse 和/或 tp_clear),在呼叫 tp_dealloc 時,物件的某些成員可能已經被清除或終結。其次,在 tp_dealloc 中,你的物件處於不穩定狀態:其引用計數為零。對非平凡物件或 API 的任何呼叫(如上述示例)都可能最終再次呼叫 tp_dealloc,導致二次釋放和崩潰。

從 Python 3.4 開始,建議不要在 tp_dealloc 中放置任何複雜的終結程式碼,而是使用新的 tp_finalize 型別方法。

參見

PEP 442 解釋了新的終結方案。

3.2. 物件表示

在 Python 中,有兩種方法可以生成物件的文字表示:repr() 函式和 str() 函式。(print() 函式只是呼叫 str()。)這些處理程式都是可選的。

reprfunc tp_repr;
reprfunc tp_str;

tp_repr 處理程式應返回一個字串物件,其中包含為其呼叫例項的表示。這是一個簡單的示例

static PyObject *
newdatatype_repr(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:%d}}",
                                self->obj_UnderlyingDatatypePtr->size);
}

如果未指定 tp_repr 處理程式,直譯器將提供一個使用型別 tp_name 和物件唯一標識值的表示。

tp_str 處理程式對於 str() 而言,就像上面描述的 tp_repr 處理程式對於 repr() 而言一樣;也就是說,當 Python 程式碼在你的物件例項上呼叫 str() 時,它會被呼叫。它的實現與 tp_repr 函式非常相似,但生成的字串旨在供人類閱讀。如果未指定 tp_str,則會使用 tp_repr 處理程式。

這是一個簡單的例子

static PyObject *
newdatatype_str(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    return PyUnicode_FromFormat("Stringified_newdatatype{{size:%d}}",
                                self->obj_UnderlyingDatatypePtr->size);
}

3.3. 屬性管理

對於每個可以支援屬性的物件,相應的型別必須提供控制屬性解析方式的函式。需要有一個函式來檢索屬性(如果定義了任何屬性),另一個函式來設定屬性(如果允許設定屬性)。刪除屬性是一種特殊情況,此時傳遞給處理程式的新值為 NULL

Python 支援兩對屬性處理程式;支援屬性的型別只需要實現其中一對的函式。區別在於其中一對將屬性名稱作為 char*,而另一對接受 PyObject*。每種型別都可以使用對其實現方便的一對。

getattrfunc  tp_getattr;        /* char * version */
setattrfunc  tp_setattr;
/* ... */
getattrofunc tp_getattro;       /* PyObject * version */
setattrofunc tp_setattro;

如果訪問物件的屬性始終是一個簡單的操作(這將在稍後解釋),則可以使用通用實現來提供屬性管理函式的 PyObject* 版本。從 Python 2.2 開始,對特定型別屬性處理程式的實際需求幾乎完全消失了,儘管有許多示例尚未更新以使用可用的新通用機制。

3.3.1. 通用屬性管理

大多數擴充套件型別只使用 簡單 屬性。那麼,是什麼使屬性簡單呢?只需滿足幾個條件

  1. 在呼叫 PyType_Ready() 時必須知道屬性的名稱。

  2. 無需特殊處理來記錄屬性是否被查詢或設定,也無需根據值採取行動。

請注意,此列表對屬性的值、值何時計算或相關資料如何儲存沒有任何限制。

當呼叫 PyType_Ready() 時,它使用型別物件引用的三個表來建立 描述符,這些描述符放置在型別物件的字典中。每個描述符控制對例項物件一個屬性的訪問。每個表都是可選的;如果所有三個都為 NULL,則該型別的例項將只具有從其基型別繼承的屬性,並且還應將 tp_getattrotp_setattro 欄位保留為 NULL,允許基型別處理屬性。

這些表被宣告為型別物件的三個欄位

struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;

如果 tp_methods 不為 NULL,它必須引用 PyMethodDef 結構的陣列。表中的每個條目都是此結構的一個例項

typedef struct PyMethodDef {
    const char  *ml_name;       /* method name */
    PyCFunction  ml_meth;       /* implementation function */
    int          ml_flags;      /* flags */
    const char  *ml_doc;        /* docstring */
} PyMethodDef;

對於型別提供的每個方法都應定義一個條目;對於從基型別繼承的方法則無需條目。末尾還需要一個額外的條目;它是一個標記陣列末尾的哨兵。ml_name 欄位的哨兵必須為 NULL

第二個表用於定義直接對映到例項中儲存的資料的屬性。支援各種原始 C 型別,並且訪問可以是隻讀或讀寫。表中的結構定義為

typedef struct PyMemberDef {
    const char *name;
    int         type;
    int         offset;
    int         flags;
    const char *doc;
} PyMemberDef;

對於表中的每個條目,將構造一個 描述符 並將其新增到型別中,該描述符將能夠從例項結構中提取值。type 欄位應包含一個型別程式碼,如 Py_T_INTPy_T_DOUBLE;該值將用於確定如何將 Python 值與 C 值相互轉換。flags 欄位用於儲存控制屬性訪問方式的標誌:可以將其設定為 Py_READONLY 以防止 Python 程式碼設定它。

使用 tp_members 表在執行時構建描述符的一個有趣優點是,以這種方式定義的任何屬性都可以透過在表中提供文字來擁有關聯的文件字串。應用程式可以使用內省 API 從類物件中檢索描述符,並使用其 __doc__ 屬性獲取文件字串。

tp_methods 表一樣,需要一個 ml_name 值為 NULL 的哨兵條目。

3.3.2. 型別特定屬性管理

為簡單起見,這裡只演示 char* 版本;名稱引數的型別是 char*PyObject* 介面之間的唯一區別。此示例實際上執行與上述通用示例相同的事情,但未使用 Python 2.2 中新增的通用支援。它解釋瞭如何呼叫處理程式函式,以便如果你確實需要擴充套件其功能,你將瞭解需要做什麼。

當物件需要屬性查詢時,將呼叫 tp_getattr 處理程式。它在與呼叫類的 __getattr__() 方法相同的情況下被呼叫。

下面是一個例子:

static PyObject *
newdatatype_getattr(PyObject *op, char *name)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    if (strcmp(name, "data") == 0) {
        return PyLong_FromLong(self->data);
    }

    PyErr_Format(PyExc_AttributeError,
                 "'%.100s' object has no attribute '%.400s'",
                 Py_TYPE(self)->tp_name, name);
    return NULL;
}

當呼叫類例項的 __setattr__()__delattr__() 方法時,將呼叫 tp_setattr 處理程式。當需要刪除屬性時,第三個引數將為 NULL。這是一個簡單地引發異常的示例;如果這確實是你想要的,則 tp_setattr 處理程式應設定為 NULL

static int
newdatatype_setattr(PyObject *op, char *name, PyObject *v)
{
    PyErr_Format(PyExc_RuntimeError, "Read-only attribute: %s", name);
    return -1;
}

3.4. 物件比較

richcmpfunc tp_richcompare;

當需要比較時,將呼叫 tp_richcompare 處理程式。它類似於富比較方法,例如 __lt__(),並且也由 PyObject_RichCompare()PyObject_RichCompareBool() 呼叫。

此函式接收兩個 Python 物件和運算子作為引數,其中運算子為 Py_EQPy_NEPy_LEPy_GEPy_LTPy_GT 之一。它應根據指定的運算子比較這兩個物件,並在比較成功時返回 Py_TruePy_False,返回 Py_NotImplemented 表示未實現比較且應嘗試另一個物件的比較方法,或在設定異常時返回 NULL

這是一個示例實現,對於內部指標大小相等則被認為相等的資料型別

static PyObject *
newdatatype_richcmp(PyObject *lhs, PyObject *rhs, int op)
{
    newdatatypeobject *obj1 = (newdatatypeobject *) lhs;
    newdatatypeobject *obj2 = (newdatatypeobject *) rhs;
    PyObject *result;
    int c, size1, size2;

    /* code to make sure that both arguments are of type
       newdatatype omitted */

    size1 = obj1->obj_UnderlyingDatatypePtr->size;
    size2 = obj2->obj_UnderlyingDatatypePtr->size;

    switch (op) {
    case Py_LT: c = size1 <  size2; break;
    case Py_LE: c = size1 <= size2; break;
    case Py_EQ: c = size1 == size2; break;
    case Py_NE: c = size1 != size2; break;
    case Py_GT: c = size1 >  size2; break;
    case Py_GE: c = size1 >= size2; break;
    }
    result = c ? Py_True : Py_False;
    return Py_NewRef(result);
 }

3.5. 抽象協議支援

Python 支援各種 抽象 “協議”;使用這些介面提供的特定介面已在抽象物件層中進行了說明。

其中許多抽象介面是在 Python 實現的早期開發中定義的。特別是,數字、對映和序列協議從一開始就是 Python 的一部分。其他協議隨著時間的推移而新增。對於依賴於型別實現中幾個處理例程的協議,舊協議被定義為由型別物件引用的可選處理程式塊。對於新協議,主型別物件中有額外的槽位,並設定一個標誌位以指示槽位存在並應由直譯器檢查。(標誌位不表示槽位值為非 NULL。該標誌可以設定為指示槽位的存在,但槽位仍可能未填充。)

PyNumberMethods   *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods  *tp_as_mapping;

如果你希望你的物件能夠像數字、序列或對映物件一樣,那麼你需要放置實現 C 型別 PyNumberMethodsPySequenceMethodsPyMappingMethods 的結構地址。你需要用適當的值填充此結構。你可以在 Python 原始碼分發的 Objects 目錄中找到每個用途的示例。

hashfunc tp_hash;

此函式,如果你選擇提供它,應返回資料型別例項的雜湊值。這是一個簡單的示例

static Py_hash_t
newdatatype_hash(PyObject *op)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    Py_hash_t result;
    result = self->some_size + 32767 * self->some_number;
    if (result == -1) {
        result = -2;
    }
    return result;
}

Py_hash_t 是一個帶符號整數型別,其寬度隨平臺而異。從 tp_hash 返回 -1 表示錯誤,這就是為什麼在雜湊計算成功時應小心避免返回它,如上所示。

ternaryfunc tp_call;

當你的資料型別的一個例項被“呼叫”時,例如,如果 obj1 是你的資料型別的一個例項,並且 Python 指令碼包含 obj1('hello'),則會呼叫 tp_call 處理程式。

此函式接受三個引數

  1. self 是作為呼叫主體的該資料型別的例項。如果呼叫是 obj1('hello'),那麼 self 就是 obj1

  2. args 是一個包含呼叫引數的元組。你可以使用 PyArg_ParseTuple() 來提取引數。

  3. kwds 是一個已傳遞的關鍵字引數字典。如果它不為 NULL 並且你支援關鍵字引數,請使用 PyArg_ParseTupleAndKeywords() 來提取引數。如果你不想支援關鍵字引數且它不為 NULL,請引發 TypeError 並附帶一條訊息,說明不支援關鍵字引數。

這是一個簡單的 tp_call 實現

static PyObject *
newdatatype_call(PyObject *op, PyObject *args, PyObject *kwds)
{
    newdatatypeobject *self = (newdatatypeobject *) op;
    PyObject *result;
    const char *arg1;
    const char *arg2;
    const char *arg3;

    if (!PyArg_ParseTuple(args, "sss:call", &arg1, &arg2, &arg3)) {
        return NULL;
    }
    result = PyUnicode_FromFormat(
        "Returning -- value: [%d] arg1: [%s] arg2: [%s] arg3: [%s]\n",
        self->obj_UnderlyingDatatypePtr->size,
        arg1, arg2, arg3);
    return result;
}
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;

這些函式提供對迭代器協議的支援。兩個處理程式都只接受一個引數,即它們被呼叫的例項,並返回一個新的引用。如果發生錯誤,它們應該設定一個異常並返回 NULLtp_iter 對應於 Python 的 __iter__() 方法,而 tp_iternext 對應於 Python 的 __next__() 方法。

任何 可迭代 物件都必須實現 tp_iter 處理程式,該處理程式必須返回一個 迭代器 物件。這裡的準則與 Python 類相同

  • 對於可以支援多個獨立迭代器的集合(例如列表和元組),每次呼叫 tp_iter 都應建立一個新的迭代器並返回。

  • 只能迭代一次的物件(通常是由於迭代的副作用,例如檔案物件)可以透過返回對自身的全新引用來實現 tp_iter——因此也應該實現 tp_iternext 處理程式。

任何 迭代器 物件都應同時實現 tp_itertp_iternext。迭代器的 tp_iter 處理程式應返回對迭代器的新引用。其 tp_iternext 處理程式應返回對迭代中下一個物件的新引用(如果存在)。如果迭代已到達末尾,tp_iternext 可以返回 NULL 而不設定異常,或者它可以在返回 NULL 之外設定 StopIteration;避免異常可以略微提高效能。如果發生實際錯誤,tp_iternext 應始終設定異常並返回 NULL

3.6. 弱引用支援

Python 的弱引用實現目標之一是允許任何型別參與弱引用機制,而不會給效能關鍵物件(如數字)帶來開銷。

參見

weakref 模組的文件。

為了使物件可弱引用,擴充套件型別必須設定 tp_flags 欄位的 Py_TPFLAGS_MANAGED_WEAKREF 位。傳統的 tp_weaklistoffset 欄位應保留為零。

具體來說,靜態宣告的型別物件將如下所示

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... other members omitted for brevity ... */
    .tp_flags = Py_TPFLAGS_MANAGED_WEAKREF | ...,
};

唯一進一步的補充是 tp_dealloc 需要清除任何弱引用(透過呼叫 PyObject_ClearWeakRefs()

static void
Trivial_dealloc(PyObject *op)
{
    /* Clear weakrefs first before calling any destructors */
    PyObject_ClearWeakRefs(op);
    /* ... remainder of destruction code omitted for brevity ... */
    Py_TYPE(op)->tp_free(op);
}

3.7. 更多建議

為了學習如何為你的新資料型別實現任何特定方法,請獲取 CPython 原始碼。轉到 Objects 目錄,然後在 C 原始檔中搜索 tp_ 加上你想要的函式(例如,tp_richcompare)。你將找到你想要實現的函式的示例。

當你需要驗證一個物件是你正在實現的型別的具體例項時,請使用 PyObject_TypeCheck() 函式。其用法示例可能如下所示

if (!PyObject_TypeCheck(some_object, &MyType)) {
    PyErr_SetString(PyExc_TypeError, "arg #1 not a mything");
    return NULL;
}

參見

下載 CPython 原始碼釋出版。

https://python.club.tw/downloads/source/

GitHub 上的 CPython 專案,CPython 原始碼在此處開發。

https://github.com/python/cpython