跳至主要内容

我的 Python 會後空翻!

為你自己學 Python

在上個章節大概看過 PyType_Type 結構並用串列的 PyList_Type 結構來當做範例,這個章節我就仿著類似的手法,做出自己的內建型別 PyKitty_type,而且還讓它可以有後空翻的效果!

先說,平常沒事請不要在 CPython 裡做這件事,這只是純粹實驗性質的練習,好玩而已。

新增型別

首先,我要建立一個 kittyobject.ckittyobject.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_greetingkitty_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__ 方法,而這個方法會對應到 PyTypeObjecttp_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 嗎?我教你啊 :)

想要成為軟體工程師嗎?這不是條輕鬆的路,除了興趣之外,還需要足夠的決心、設定目標並持續學習,我們的ASTROCamp 軟體工程師培訓營提供專業的前後端課程培訓,幫助你在最短時間內建立正確且扎實的軟體開發技能,有興趣而且不怕吃苦的話不妨來試試看!