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 */
struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;
// Strong reference on a heap type, borrowed reference on a static type
struct _typeobject *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;
PyObject *tp_subclasses;
PyObject *tp_weaklist;
destructor tp_del;
/* Type attribute cache version tag. Added in version 2.6 */
unsigned int tp_version_tag;
destructor tp_finalize;
vectorcallfunc tp_vectorcall;
/* bitset of which type-watchers care about this type */
unsigned char tp_watched;
} 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(newdatatypeobject *obj)
{
free(obj->obj_UnderlyingDatatypePtr);
Py_TYPE(obj)->tp_free((PyObject *)obj);
}
如果你的型別支援垃圾回收,則解構函式應在清除任何成員欄位之前呼叫 PyObject_GC_UnTrack()
。
static void
newdatatype_dealloc(newdatatypeobject *obj)
{
PyObject_GC_UnTrack(obj);
Py_CLEAR(obj->other_obj);
...
Py_TYPE(obj)->tp_free((PyObject *)obj);
}
釋放器函式的一個重要要求是,它不能影響任何未決的異常。這很重要,因為當直譯器展開 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(obj)->tp_free((PyObject*)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(newdatatypeobject *obj)
{
return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:%d}}",
obj->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(newdatatypeobject *obj)
{
return PyUnicode_FromFormat("Stringified_newdatatype{{size:%d}}",
obj->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. 通用屬性管理¶
大多數擴充套件型別僅使用簡單屬性。那麼,是什麼使屬性變得簡單呢? 只有幾個條件必須滿足:
當呼叫
PyType_Ready()
時,必須知道屬性的名稱。不需要特殊處理來記錄已查詢或設定的屬性,也不需要根據該值採取操作。
請注意,此列表不限制屬性的值、何時計算值或如何儲存相關資料。
當呼叫 PyType_Ready()
時,它使用型別物件引用的三個表來建立 描述符,這些描述符放置在型別物件的字典中。每個描述符控制對例項物件的一個屬性的訪問。每個表都是可選的;如果所有三個表都是 NULL
,則該型別的例項將僅具有從其基型別繼承的屬性,並且應將 tp_getattro
和 tp_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_INT
或 Py_T_DOUBLE
;該值將用於確定如何將 Python 值轉換為 C 值以及從 C 值轉換為 Python 值。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(newdatatypeobject *obj, char *name)
{
if (strcmp(name, "data") == 0)
{
return PyLong_FromLong(obj->data);
}
PyErr_Format(PyExc_AttributeError,
"'%.100s' object has no attribute '%.400s'",
Py_TYPE(obj)->tp_name, name);
return NULL;
}
當呼叫類例項的 __setattr__()
或 __delattr__()
方法時,將呼叫 tp_setattr
處理程式。當應該刪除屬性時,第三個引數將為 NULL
。這是一個僅引發異常的示例;如果這真的是你想要的全部,則 tp_setattr
處理程式應設定為 NULL
。
static int
newdatatype_setattr(newdatatypeobject *obj, 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_EQ
、Py_NE
、Py_LE
、Py_GE
、Py_LT
或 Py_GT
之一。它應該根據指定的運算子比較這兩個物件,如果比較成功,則返回 Py_True
或 Py_False
,返回 Py_NotImplemented
表示未實現比較並且應嘗試另一個物件的比較方法,或者如果設定了異常,則返回 NULL
。
這是一個示例實現,適用於當內部指標的大小相等時被認為相等的資料型別:
static PyObject *
newdatatype_richcmp(newdatatypeobject *obj1, newdatatypeobject *obj2, int op)
{
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;
Py_INCREF(result);
return result;
}
3.5. 抽象協議支援¶
Python 支援各種抽象“協議”;使用這些介面提供的特定介面記錄在抽象物件層中。
在 Python 實現的早期開發階段,就定義了許多這樣的抽象介面。特別是,數字、對映和序列協議從 Python 的一開始就存在。隨著時間的推移,又添加了其他協議。對於依賴於型別實現中的多個處理例程的協議,較舊的協議被定義為型別物件引用的可選處理程式塊。對於較新的協議,主型別物件中有額外的槽位,並設定一個標誌位來指示這些槽位存在並且應該由直譯器檢查。(標誌位並不表示槽位值是非 NULL
的。該標誌可能被設定為指示槽位的存在,但槽位可能仍然是未填充的。)
PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
如果您希望您的物件能夠像數字、序列或對映物件一樣操作,那麼您需要分別放置一個實現 C 型別 PyNumberMethods
、 PySequenceMethods
或 PyMappingMethods
的結構的地址。 您需要自行填寫此結構中的適當值。您可以在 Python 原始碼分發的 Objects
目錄中找到每個用法的示例。
hashfunc tp_hash;
如果您選擇提供此函式,它應該為您的資料型別的例項返回一個雜湊值。 這是一個簡單的例子
static Py_hash_t
newdatatype_hash(newdatatypeobject *obj)
{
Py_hash_t result;
result = obj->some_size + 32767 * obj->some_number;
if (result == -1)
result = -2;
return result;
}
Py_hash_t
是一個有符號整數型別,其寬度因平臺而異。從 tp_hash
返回 -1
表示錯誤,這就是為什麼在雜湊計算成功時,您應該小心避免返回它的原因,如上所示。
ternaryfunc tp_call;
當您的資料型別的例項被“呼叫”時,會呼叫此函式。例如,如果 obj1
是您的資料型別的一個例項,並且 Python 指令碼包含 obj1('hello')
,則會呼叫 tp_call
處理程式。
此函式接受三個引數
self 是作為呼叫主題的資料型別的例項。如果呼叫是
obj1('hello')
,則 self 是obj1
。args 是包含呼叫引數的元組。 您可以使用
PyArg_ParseTuple()
來提取引數。kwds 是傳遞的關鍵字引數字典。如果它不是
NULL
並且您支援關鍵字引數,請使用PyArg_ParseTupleAndKeywords()
來提取引數。 如果您不想支援關鍵字引數,並且它不是NULL
,請引發TypeError
並顯示一條訊息,說明不支援關鍵字引數。
這是一個簡單的 tp_call
實現
static PyObject *
newdatatype_call(newdatatypeobject *obj, PyObject *args, PyObject *kwds)
{
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",
obj->obj_UnderlyingDatatypePtr->size,
arg1, arg2, arg3);
return result;
}
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;
這些函式為迭代器協議提供支援。兩個處理程式都只接受一個引數,即被呼叫的例項,並返回一個新的引用。如果發生錯誤,它們應該設定一個異常並返回 NULL
。tp_iter
對應於 Python 的 __iter__()
方法,而 tp_iternext
對應於 Python 的 __next__()
方法。
任何 可迭代 物件都必須實現 tp_iter
處理程式,該處理程式必須返回一個 迭代器 物件。 這裡適用與 Python 類相同的指導原則
對於可以支援多個獨立迭代器的集合(例如列表和元組),每次呼叫
tp_iter
時,都應該建立並返回一個新的迭代器。只能迭代一次的物件(通常是由於迭代的副作用,例如檔案物件)可以透過返回對自身的新的引用來實現
tp_iter
,因此也應該實現tp_iternext
處理程式。
任何 迭代器 物件都應該實現 tp_iter
和 tp_iternext
。 迭代器的 tp_iter
處理程式應該返回一個指向該迭代器的新引用。如果存在下一個物件,則其 tp_iternext
處理程式應該返回指向迭代中下一個物件的新引用。如果迭代已到達末尾,tp_iternext
可以返回 NULL
而不設定異常,或者它可以設定 StopIteration
*此外* 還返回 NULL
;避免異常可以產生稍微好一點的效能。 如果發生實際錯誤,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(TrivialObject *self)
{
/* Clear weakrefs first before calling any destructors */
PyObject_ClearWeakRefs((PyObject *) self);
/* ... remainder of destruction code omitted for brevity ... */
Py_TYPE(self)->tp_free((PyObject *) self);
}
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 原始碼釋出版本。
- GitHub 上的 CPython 專案,其中開發了 CPython 原始碼。