我們都知道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