iT邦幫忙

2024 iThome 鐵人賽

DAY 6
1
Python

為你自己讀 CPython 原始碼系列 第 6

Day 6 - 我的 Python 會後空翻!

  • 分享至 

  • xImage
  •  

本文同步刊載於 「為你自己學 Python - 我的 Python 會後空翻!

我的 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 - 我的 Python 會後空翻!


上一篇
Day 5 - 全部都是物件!(下)
下一篇
Day 7 - 匯入模組的時候...
系列文
為你自己讀 CPython 原始碼31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言