python擴展學習之路->使用pyhton/c api實現一個簡單的單鏈表

我們都知道python的執行效率相較於其他比較出名的語言是比較低的,但是因爲開發效率高加上簡單易學,讓其成爲比較流行的編程語言,而對於一個項目來說,一般只有20%的性能瓶頸是在語言層面的,我們只有找到這20%的性能瓶頸,用執行效率比較高的c去實現就行了,好在CPython提供了c/api,方便我們用c去擴展python。本例子主要是用於學習python c/api(案例代碼地址:https://github.com/marskang/python-ext-linklist.git)。

鏈表是一種物理存儲單元上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的。既然我們要寫單鏈表,首先要實現的是單鏈表的數據結構,我們可以按照教科書上面的鏈表結構來定義:

typedef struct LinkList {
     int Element;
     struct LinkList * next;
} LinkList;

這樣一個單鏈表的數據結構雖然定義好了,但是因爲我們是用c去擴展python,這樣的數據結構,python解釋器並不能識別,我們需要將該數據結構改爲能夠轉爲python對象的數據結構。可以參考官方的例子https://docs.python.org/2/extending/newtypes.html?highlight=noddy
來寫。首先需要在數據結構的第一行加入PyObject_HEAD宏,因爲我們的數據結構是可以像list一樣放進去任何python對象的,所以我們需要將int轉改爲PyObject:

typedef struct PyLinkList_Node {
    PyObject_HEAD
    PyObject* content;
    struct PyLinkList_Node* next;
} PyLinkList_Node;

這樣一個簡單的python的單鏈表的數據結構就定義好了,但是我們由於的鏈表還有一些添加,獲取長度,迭代等方法,所以最終的結構如下:

typedef struct PyLinkList_Node {
    PyObject_HEAD
    Py_ssize_t count;
    PyObject* content;
    struct PyLinkList_Node* cursor;
    struct PyLinkList_Node* next;
    struct PyLinkList_Node* tail;
} PyLinkList_Node;

這樣雖然數據結構定義好了,但是python解釋器還是沒辦法將其轉爲python對象,我們可以還需要定義PyTypeObject才行,我們單鏈表的PyTypeObject實現如下:

PyTypeObject pyLinkList_NodeType = {
        PyVarObject_HEAD_INIT(NULL, 0)
        "PyLinkList",                                   /* tp_name */
        sizeof(PyLinkList_Node),                        /* tp_basicsize */
        0,                                              /* tp_itemsize */
        (destructor)pyLinkList_dealloc,                 /* tp_dealloc */
        0,                                              /* tp_print */
        0,                                              /* tp_getattr */
        0,                                              /* tp_setattr */
        0,                                              /* tp_reserved */
        0,                                              /* tp_repr */
        0,                                              /* tp_as_number */
        &linklist_as_sequence,                          /* tp_as_sequence */
        0,                                              /* tp_as_mapping */
        0,                                              /* tp_hash */
        0,                                              /* tp_call */
        0,                                              /* tp_str */
        0,                                              /* tp_getattro */
        0,                                              /* tp_setattro */
        0,                                              /* tp_as_buffer */
        Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE,         /* tp_flags */
        0,                                              /* tp_doc */
        0,                                              /* tp_traverse */
        0,                                              /* tp_clear */
        0,                                              /* tp_richcompare */
        0,                                              /* tp_weaklistoffset */
        (getiterfunc)pyLinkList_getiter,                /* tp_iter */
        (iternextfunc)pyLinkList_iternext,              /* tp_iternext */
        pyLinkList_methods,                             /* tp_methods */
        0,                                              /* tp_members */
        0,                                              /* tp_getset */
        0,                                              /* tp_base */
        0,                                              /* tp_dict */
        0,                                              /* tp_descr_get */
        0,                                              /* tp_descr_set */
        0,                                              /* tp_dictoffset */
        (initproc)pyLinkList_init,                      /* tp_init */
        0,                                              /* tp_alloc */
        0,                                              /* tp_new */
        0                                               /* tp_free */
};

這樣一個單鏈表的數據類型就定義好了,當鏈表實例化的時候,會調用pyLinkList_init方法,返回一個頭節點的指針。使用for循環遍歷的時候會先調用pyLinkList_getiter,然後每次再調用pyLinkList_iternext,直到返回NULL,我們是鏈表,我們希望用數組下標的方式來訪問第n個元素,那當我使用數組下標訪問鏈表元素的時候,PyTypeObject會先找到一開始定義好的linklist_as_sequence,然後找到對應的方法。對象被回收時,會調用pyLinkList_dealloc方法,pyLinkList_methods是定義好的對象的方法。

我們要實現的是單鏈表,單鏈表首先要實現的是能夠添加數據吧,所以我們需要先實現一個append方法。

void pyLinkList_append(PyLinkList_Node* self, PyObject* obj) {
    PyLinkList_Node* node = (PyLinkList_Node*) (pyLinkList_NodeType.tp_alloc(&pyLinkList_NodeType, 0));
    if (!node) {
        return;
    }
    Py_INCREF(self);
    node->content = obj;
    node->next = NULL;
    self->tail->next = node;
    self->tail = node;
    self->count++;
}

方法定義好了,我們需要要將其跟PyLinkList_Node關聯起來,我們需要先定義個PyMethodDef數組將所有的方法與其關聯起來。

static PyMethodDef pyLinkList_methods[] = {
    {"append", (PyCFunction)pyLinkList_append, METH_O, "append item"},
    {NULL},
};

每一個數組元素代表一個元素,以一組都是NULL的結束,第一個元素代表方法名(我們這裏是append),第二個是c實現的方法,第三個是參數類型(我們這裏是METH_O,代表傳的是一個python object,再比如METH_NOARGS代表沒有參數,具體的可以參考api),最後一個是方法的doc。然後將pyLinkList_methods添加到PyTypeObject即可,我們上面實現的PyTypeObject已將其添加過了。

既然是一個鏈表的數據結構,我們希望能夠將其放入for循環中去迭代,首先我們需要先實現getiterfunc,返回鏈表的頭節點地址,讓其變成一個可迭代的對象。

PyObject* pyLinkList_getiter(PyLinkList_Node* self) {
    Py_INCREF(self);
    return (PyObject*)self;
}

然後pyLinkList_getiter放入PyTypeObject結構中。

然後需要實現一個iternextfunc,是其每次迭代調的方法,直到返回NULL,迭代結束。

PyObject* pyLinkList_iternext(PyLinkList_Node* self) {
    PyLinkList_Node* next;
    if(!self->next || !self->cursor) {
        //將遊標重置
        self->cursor = self;
        return NULL;
    }
    if (self == self->cursor) {
        // 過濾頭節點
        self->cursor = self->cursor->next;
        next = self->next;
    } else {
        next = self->cursor;
    }
    self->cursor = self->cursor->next;
    return next->content;
}

同樣需要將其放入PyTypeObject中。
我們既然是一個鏈表,有時候我們想通過數組下標的的形式,來訪問鏈表的第n個元素,這裏我們需要先定義好PySequenceMethods:

static PySequenceMethods linklist_as_sequence = {
    (lenfunc)PyLinkList_count,                       /* sq_length */
    0,                                               /* sq_concat */
    0,                                               /* sq_repeat */
    (ssizeargfunc)pyLinkList_item,                   /* sq_item */
    0,                                               /* sq_slice */
    0,                                               /* sq_ass_item */
    0,                                               /* sq_ass_slice */
    0,                                               /* sq_contains */
    0,                                               /* sq_inplace_concat */
    0,                                               /* sq_inplace_repeat */
};

這裏只是先實現了,sq_length(通過len方法調用,例如len(l)),跟sq_item(通過數組下標調用,例如l[0]),兩個方法如下:

Py_ssize_t PyLinkList_count(PyLinkList_Node* self) {
    return self->count;
}

static PyObject *indexerr = NULL;
PyObject* pyLinkList_item(PyLinkList_Node* self, Py_ssize_t index) {
    if (index < 0 || index >= self->count) {
        if (indexerr == NULL) {
            indexerr = PyString_FromString("link list index out of range");
            if (indexerr == NULL)
                return NULL;
        }
        //返回數組越界異常
        PyErr_SetObject(PyExc_IndexError, indexerr);
        return NULL;
    }
    Py_ssize_t start = 0;
    //過濾頭節點
    PyLinkList_Node* p = self->next;
    while(p) {
        if (start == index) {
            Py_INCREF(p->content);
            return p->content;
        }
        p = p->next;
        start++;
    }
    return NULL;
}

這樣我們方法都寫好了,但是我們如何讓其變成python的模塊跟python對象呢,首先我們還需要繼續實現PyMethodDef數組,定義模塊的方法,由於我們本模塊沒有單獨的方法,只有一個鏈表類型(方法都是跟鏈表對象綁定的),所以數組只有一組爲NULL的元素。

static PyMethodDef pyLinkList_module_methods[] = {
    {NULL},
};

然後再實現一個PyMODINIT_FUNC的方法,我們這裏模塊命名爲linklist,所以方法名需要爲initlinklist(init+模塊名):

PyMODINIT_FUNC initlinklist(void) {
    pyLinkList_NodeType.tp_new = PyType_GenericNew;
    if (PyType_Ready(&pyLinkList_NodeType) < 0) {
        return;
    }
    PyObject* m = Py_InitModule3("linklist", pyLinkList_module_methods, "test");
    Py_INCREF(&pyLinkList_NodeType);
    //給模塊添加一個python類型,類型名爲LinkList
    PyModule_AddObject(m, "LinkList", (PyObject*)&pyLinkList_NodeType);
}

這樣我們的代碼就編寫完成了,下面就是編譯鏈接執行了,有兩種方法,一種是通過gcc編譯成so調用:

gcc -shared -fPIC link_list.c -I/usr/include/python2.7/ -L/usr/lib -lpython2.7 -o linklist.so

還有一種是通過distutils,將其加入python庫使用,首先需要新建一個setup.py文件,內容如下:

from distutils.core import setup, Extension

setup(name="linklist", version="1.0",ext_modules=[Extension("linklist", ["link_list.c"]),])

之後我們就可以使用c實現的單鏈表了:

# -*- coding: utf-8 -*-

import linklist

l = linklist.LinkList()
l.append("張三")
l.append("12")
l.append(666)

for i in l:
    print i

print len(l)
print l[1]

輸入結果如下:

$ python test.py 
張三
12
666
3
12

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章