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