本文同步刊載於 「為你自己學 Python - 我的 Python 會後空翻!」
在上個章節大概看過 PyType_Type
結構並用串列的 PyList_Type
結構來當做範例,這個章節我就仿著類似的手法,做出自己的內建型別 PyKitty_type
,而且還讓它可以有後空翻的效果!
先說,平常沒事請不要在 CPython 裡做這件事,這只是純粹實驗性質的練習,好玩而已。
首先,我要建立一個 kittyobject.c
跟 kittyobject.h
,其中 .h
檔比較簡單,就擺在 Includes
目錄裡就好:
// 檔案:Includes/kittyobject.h
#ifndef Py_KITTYOBJECT_H
#define Py_KITTYOBJECT_H
#include "Python.h"
extern PyTypeObject PyKitty_Type;
#endif
我打算宣告一個叫做 PyKitty_Type
的型別,這要叫什麼名字你可以自己決定,只要不要跟其它內建的型別的名字重複就好。接下來實作的 .c
檔比較囉嗦一點,我就把它跟放在跟串列 listobject.c
相同的 Objects
目錄裡:
// 檔案: Objects/kittyobject.c
#include <Python.h>
#include "kittyobject.h"
typedef struct {
PyObject_HEAD
} KittyObject;
這裡我就跟 PyListObject
一樣,定義一個叫做 KittyObject
的結構,裡面如果還想加其它成員變數可以加在這裡。這裡我使用 PyObject_HEAD
而不是像 PyListObject
一樣使用 PyObject_VAR_HEAD
巨集,是因為 PyObject_VAR_HEAD
多了一個 ob_size
用來記錄物件的大小,以我的 PyKittyObject
來說應該不需要它,所以我還擇更簡單的 PyObject_HEAD
巨集。
再來,我希望這個型別所產生的物件除了能後空翻之外,還會很有禮貌的打招呼,這裡我先建立兩個簡單的函數:
// 檔案: Objects/kittyobject.c
static PyObject *
kitty_greeting(KittyObject *self, PyObject *Py_UNUSED(ignored))
{
printf("哈囉,凱蒂\n");
Py_RETURN_NONE;
}
static PyObject *
kitty_backflip(KittyObject *self, PyObject *Py_UNUSED(ignored))
{
printf("我的貓會後空翻!後空翻!後空翻!\n");
Py_RETURN_NONE;
}
函數的命名也可以自己決定,同樣也只要不重複即可,我這裡仿著 listobject.c
裡的函數命名風格,在函數前面加上型別的名字,像是 kitty_greeting
以及 kitty_backflip
。裡面的實作也很簡單,就只是用 printf()
函數印出幾個字而已。
然後,我希望待會在 REPL 印出來的時候可以看起來跟別人不太一樣,所以我先準自己的 tp_repl
成員變數的函數,這裡我就叫它 kitty_repr
:
// 檔案: Objects/kittyobject.c
static PyObject *
kitty_repr(KittyObject *self)
{
return PyUnicode_FromString("❤ Hello Kitty ٩(ˊᗜˋ*)و🍟");
}
照理 __repr__
應該要印出給開發人員看的東西,通常這裡會印出這個物件的記憶體位置之類的資訊,我這裡只是為了好玩所以故意放了一些不實用的字。最後,我還參考 listobject.c
裡的 list_dealloc
函數,實作一個 kitty_dealloc
函數,主要用除是釋放記憶體:
// 檔案: Objects/kittyobject.c
static void
kitty_dealloc(KittyObject *self)
{
Py_TYPE(self)->tp_free((PyObject *)self);
}
準備的差不多了,是時候來準備 PyKitty_Type
裡面的內容了:
// 檔案: Objects/kittyobject.c
PyTypeObject PyKitty_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
.tp_name = "kitty",
.tp_basicsize = sizeof(KittyObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_alloc = PyType_GenericAlloc,
.tp_new = PyType_GenericNew,
.tp_free = PyObject_Del,
.tp_dealloc = (destructor) kitty_dealloc,
.tp_doc = "哈囉,凱蒂",
};
這裡我是仿著 listobject.c
裡的 PyList_Type
的寫法再做一點微調。因為我想要有個跟字串的 <class 'str'>
或是串列 <class 'list'>
類似的效果,所以這裡我把 tp_name
設定成 "kitty"
。
但我們在上個章節也學到,並不是這樣定義了 kitty_greeting
、kitty_backflip
或是 kitty_repr
就能被呼叫到,得把它們掛到我寫的這個 PyKitty_Type
身上:
// 檔案: Objects/kittyobject.c
PyTypeObject PyKitty_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"kitty",
// ... 略 ...
.tp_doc = "哈囉,凱蒂",
.tp_repr = (reprfunc)kitty_repr,
};
我在最後面先把剛剛寫的 kitty_repr
掛在 tp_repr
成員變數上。而另自己寫寫的兩個方法應該是要掛到 tp_methods
上,所以我得先做點準備:
// 檔案: Objects/kittyobject.c
static PyMethodDef kitty_methods[] = {
{"greeting", (PyCFunction)kitty_greeting, METH_NOARGS, "哈囉"},
{"backflip", (PyCFunction)kitty_backflip, METH_NOARGS, "後空翻"},
{NULL, NULL}
};
我先把準備給 kitty
型別用的方法放在 kitty_methods[]
裡,這個寫法我是參考上個章節在 listobject.c
裡的 list_methods
。接著,把它掛到 tp_methods
成員變數上:
// 檔案: Objects/kittyobject.c
PyTypeObject PyKitty_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"kitty",
// ... 略 ...
.tp_doc = "哈囉,凱蒂",
.tp_repr = (reprfunc)kitty_repr,
.tp_methods = kitty_methods,
};
實作的程式碼的部份差不多就先這樣。
接下來,因為我希望讓它可以像串列或 Tuple 一樣,可以不需要 import
就能用的變成內建的型別,所以需要再到 Python/bltinmodule.c
找到 _PyBuiltin_Init()
函數,在這個函數裡你應該會看到一個 SETBUILTIN
巨集,把我們的 PyKitty_Type
掛上去:
// 檔案:Python/bltinmodule.c
PyObject *
_PyBuiltin_Init(PyInterpreterState *interp)
{
// ... 略 ...
SETBUILTIN("tuple", &PyTuple_Type);
SETBUILTIN("type", &PyType_Type);
SETBUILTIN("zip", &PyZip_Type);
SETBUILTIN("kitty", &PyKitty_Type);
debug = PyBool_FromLong(config->optimization_level == 0);
if (PyDict_SetItemString(dict, "__debug__", debug) < 0) {
Py_DECREF(debug);
return NULL;
}
// ... 略 ...
}
別忘了把 .h
檔加進來:
// 檔案:Python/bltinmodule.c
#include "kittyobject.h"
最後,再調整一下 Makefile:
// 檔案:Makefile.pre.in
OBJECT_OBJS= \
// ... 略 ...
Objects/unicodeobject.o \
Objects/unicodectype.o \
Objects/unionobject.o \
Objects/weakrefobject.o \
Objects/kittyobject.o \
@PERF_TRAMPOLINE_OBJ@
把 Objects/kittyobject.o
加上去。
差不多了,準備開始進行編譯:
$ ./configure
$ make
假設一切都順利的話,我們就可以來試試看了:
>>> kitty
<class 'kitty'>
>>> type(kitty)
<class 'type'>
喔耶!的確有 kitty
這個型別了,而且還不用 import
就有了。再試試看:
>>> help(kitty)
class kitty(object)
| 哈囉,凱蒂
|
| Methods defined here:
|
看起來 tp_doc
成員變數也有正常運作,好啦,是時候讓主角登場了:
>>> cc = kitty()
>>> cc
❤ Hello Kitty ٩(ˊᗜˋ*)و🍟
你看看,我們自己做的型別就是跟別人的不一樣!再執行其它的方法看看:
>>> cc.greeting()
哈囉,凱蒂
>>> cc.backflip()
我的貓會後空翻!後空翻!後空翻!
看起來沒問題,只是目前的 kitty
型別還沒辦法帶參數初始化,這樣打招呼的時候就只能固定的說「哈囉,凱蒂」有點不好玩,接下來我們來試著加上帶參數初始化的功能。
目前的 kitty
型別有點無聊,只能固定的打招呼,我希望可以讓它用起來的手感像這樣:
c = kitty()
c.greeting() # 哈囉!
k = kitty("凱蒂")
k.greeting() # 哈囉,凱蒂!
有給參數就會在打招呼的時候會帶上名字,沒有的話就禮貌性的說聲哈囉就好。
要做到這件事,目前設計的 KittyObject
結構裡面沒地方可以放名字,所以我加上一個 name
成員變數,待會做初始化的時候可以把傳入的字串指定給它:
// 檔案:Python/kitteyobject.c
typedef struct {
PyObject_HEAD
PyObject *name;
} KittyObject;
接下來,我們在寫 Python 的時候應該都知道在建立物件的時候要帶額外的參數給它話,需要在類別裡加上 __init__
方法,而這個方法會對應到 PyTypeObject
的 tp_init
的成員變數,所以我先加上這個功能:
// 檔案:Python/kitteyobject.c
static int
kitty_init(KittyObject *self, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"name", NULL};
PyObject *name = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|U", kwlist, &name)) {
return -1;
}
if (name != NULL) {
Py_INCREF(name);
}
Py_XSETREF(self->name, name);
return 0;
}
簡單說明如下:
PyArg_ParseTupleAndKeywords()
函數的用途是把傳入的參數解析成 Python 的物件,而Py_Py_INCREF()
函數的用途是增加物件的參考計數,這樣才不會在函數結束的時候被 Python 的回收機制給回收掉。Py_XSETREF()
函數,把 name
設定到成員變數的 name
。再來,把這個函數掛到 PyKitty_Type
裡的 tp_init
成員變數上:
// 檔案:Python/kitteyobject.c
PyTypeObject PyKitty_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0) "kitty",
// ... 略 ...
.tp_repr = (reprfunc)kitty_repr,
.tp_methods = kitty_methods,
.tp_init = (initproc)kitty_init,
};
最後再調整一下原本的 kitty_greeting
函數,讓它可以印出名字:
// 檔案:Python/kitteyobject.c
static PyObject *
kitty_greeting(KittyObject *self, PyObject *Py_UNUSED(ignored))
{
if (self->name != NULL) {
printf("哈囉,%s!\n", PyUnicode_AsUTF8(self->name));
} else {
printf("哈囉!\n");
}
Py_RETURN_NONE;
}
重新編譯一次之後來試試看:
>>> c = kitty()
>>> c.greeting()
哈囉!
>>> k = kitty("凱蒂")
>>> k.greeting()
哈囉,凱蒂!
>>>
喔耶!成功了,下次你可以約朋友來家裡看你家的 Python 後空翻了!
完整程式碼:
// 檔案:Includes/kittyobject.h
#ifndef Py_KITTYOBJECT_H
#define Py_KITTYOBJECT_H
#include "Python.h"
extern PyTypeObject PyKitty_Type;
#endif
// 檔案:Python/kitteyobject.c
#include <Python.h>
#include "kittyobject.h"
typedef struct {
PyObject_HEAD
PyObject *name;
} KittyObject;
static PyObject *
kitty_greeting(KittyObject *self, PyObject *Py_UNUSED(ignored))
{
if (self->name != NULL) {
printf("哈囉,%s!\n", PyUnicode_AsUTF8(self->name));
} else {
printf("哈囉!\n");
}
Py_RETURN_NONE;
}
static PyObject *
kitty_backflip(KittyObject *self, PyObject *Py_UNUSED(ignored))
{
printf("我的貓會後空翻!後空翻!後空翻!\n");
Py_RETURN_NONE;
}
static PyObject *
kitty_repr(KittyObject *self)
{
return PyUnicode_FromString("❤ Hello Kitty ٩(ˊᗜˋ*)و🍟");
}
static void
kitty_dealloc(KittyObject *self)
{
Py_XDECREF(self->name);
Py_TYPE(self)->tp_free((PyObject *)self);
}
static int
kitty_init(KittyObject *self, PyObject *args, PyObject *kwds)
{
static char *kwlist[] = {"name", NULL};
PyObject *name = NULL;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|U", kwlist, &name)) {
return -1;
}
if (name != NULL) {
Py_INCREF(name);
}
Py_XSETREF(self->name, name);
return 0;
}
static PyMethodDef kitty_methods[] = {
{"greeting", (PyCFunction)kitty_greeting, METH_NOARGS, "哈囉"},
{"backflip", (PyCFunction)kitty_backflip, METH_NOARGS, "後空翻"},
{NULL, NULL}
};
PyTypeObject PyKitty_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0) "kitty",
.tp_basicsize = sizeof(KittyObject),
.tp_itemsize = 0,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_alloc = PyType_GenericAlloc,
.tp_new = PyType_GenericNew,
.tp_dealloc = (destructor) kitty_dealloc,
.tp_free = PyObject_Del,
.tp_doc = "哈囉,凱蒂",
.tp_repr = (reprfunc)kitty_repr,
.tp_methods = kitty_methods,
.tp_init = (initproc)kitty_init,
};
本文同步刊載於 「為你自己學 Python - 我的 Python 會後空翻!」