用 Cython 造个轮子
作者:Nugine
知乎专栏:https://zhuanlan.zhihu.com/c_168195059
在本篇文章中,我要向你展示使用 Cython 扩展 Python 的技巧。
如果你同时有 C/C++和 Python 的编码能力,我相信你会喜欢这个的。
我们要造的轮子是一个最简单的栈的实现,用 C/C++来编写能够减小不必要的开销,带来显著的加速。
步骤
建立目录
编写 C++文件
编写 pyx 文件
直接编译
测试
1. 建立目录
首先,建立我们的工作目录。
mkdir pystack
cd pystack
32 位版本和 64 位版本会带来不同的问题。我的 C 库是 32 位的,所以 python 库必须也是 32 位。
使用 pipenv 指定 python 版本,并安装 Cython。
pipenv --python P:\Py3.6.5\python.exe
pipenv install Cython
2. 编写 C++文件
按 Python 官方文档,这里 C++必须用 C 的方式编译,所以需要加上 extern "C"。
"c_stack.h"
extern "C"{
class C_Stack {
private:
struct Node {
PyObject* val;
Node* prev;
};
Node* tail;
public:
C_Stack();
~C_Stack();
PyObject* peek();
void push(PyObject* val);
PyObject* pop();
};
}
"c_stack.cpp"
extern "C"{
}
C_Stack::C_Stack() {
tail = new Node;
tail->prev = NULL;
tail->val = NULL;
};
C_Stack::~C_Stack() {
Node *t;
while(tail!=NULL){
t=tail;
tail=tail->prev;
delete t;
}
};
PyObject* C_Stack::peek() {
return tail->val;
}
void C_Stack::push(PyObject* val) {
Node* nt = new Node;
nt->prev = tail;
nt->val = val;
tail = nt;
}
PyObject* C_Stack::pop() {
Node* ot = tail;
PyObject* val = tail->val;
if (tail->prev != NULL) {
tail = tail->prev;
delete ot;
}
return val;
}
最简单的栈实现,只有 push,peek,pop 三个接口,作为示例足够了。
3. 编写 pyx 文件
Cython 使用 C 与 Python 混合的语法简化了扩展 Python 的步骤。
编写起来十分简单,前提是事先了解它的语法。
"pystack.pyx"
# distutils: language=c++
# distutils: sources = c_stack.cpp
from cpython.ref cimport PyObject,Py_INCREF,Py_DECREF
cdef extern from 'c_stack.h':
cdef cppclass C_Stack:
PyObject* peek();
void push(PyObject* val);
PyObject* pop();
class StackEmpty(Exception):
pass
cdef class Stack:
cdef C_Stack _c_stack
cpdef object peek(self):
cdef PyObject* val
val=self._c_stack.peek()
if val==NULL:
raise StackEmpty
return <object>val
cpdef object push(self,object val):
Py_INCREF(val);
self._c_stack.push(<PyObject*>val);
return None
cpdef object pop(self):
cdef PyObject* val
val=self._c_stack.pop()
if val==NULL:
raise StackEmpty
cdef object rv=<object>val;
Py_DECREF(rv)
return rv
分为四个部分:
注释指定相应的 cpp 文件。
从 CPython 导入 C 符号:PyObject,Py_INCREF,Py_DECREF。
从"c_stack.h"导入 C 符号: C_Stack,以及它的接口。
将其包装为 Python 对象。
注意点:
在 C 实现中,当栈为空时,返回了空指针。Python 实现中检查空指针,并抛出异常 StackEmpty.
PyObject* 和 object 并不等同,需要做类型转换。
push 和 pop 时要正确操作引用计数,否则会让 Python 解释器直接崩溃。一开始不知道这个,懵逼好久,偶然间看到报错与 gc 有关,才想到引用计数的问题。
4. 直接编译
pipenv run cythonize -a -i pystack.pyx
生成三个文件: pystack.cpp,pystack.html,pystack.cp36-win32.pyd
pyx 编译到 cpp,再由 C 编译器编译为 pyd。
html 是 cython 提示,指出 pyx 代码中与 python 的交互程度。
pyd 就是最终的 Python 库了。
5. 测试一下
"test.py"
from pystack import *
st=Stack()
print(dir(st))
try:
st.pop()
except StackEmpty as exc:
print(repr(exc))
print(type(st.pop))
for i in ['1',1,[1.0],1,dict(a=1)]:
st.push(i)
while True:
print(st.pop())
pipenv run python test.py
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
'__ne__', '__new__', '__pyx_vtable__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', 'peek', 'pop', 'push']
<class 'list'>
{'a': 1}
1
[1.0]
1
1
Traceback (most recent call last):
File "test.py", line 13, in <module>
print(st.pop())
File "pystack.pyx", line 32, in pystack.Stack.pop
cpdef object pop(self):
File "pystack.pyx", line 36, in pystack.Stack.pop
raise StackEmpty
pystack.StackEmpty
与正常 Python 对象表现相同,完美!
6. 应用
pipenv run python test_polish_notation.py
from operator import add, sub, mul, truediv
from fractions import Fraction
from pystack import Stack
def main():
exp = input('exp: ')
val = eval_exp(exp)
print(f'val: {val}')
op_map = {
'+': add,
'-': sub,
'*': mul,
'/': truediv
}
def convert(exp):
for it in reversed(exp.split(' ')):
if it in op_map:
yield True, op_map[it]
else:
yield False, Fraction(it)
def eval_exp(exp):
stack = Stack()
for is_op, it in convert(exp):
if is_op:
left = stack.pop()
right = stack.pop()
stack.push(it(left, right))
else:
stack.push(it)
return stack.pop()
if __name__ == '__main__':
main()
# exp: + 5 - 2 * 3 / 4 7
# val: 37/7
本篇文章展示了最简单的 Cython 造轮子技巧,希望能为即将进坑和已经进坑的同学提供一块垫脚石。如果对你有所帮助,请点赞和收藏。
Python的爱好者社区历史文章大合集:
关注后在公众号内回复“ 课程 ”即可获取:
小编的转行入职数据科学(数据分析挖掘/机器学习方向)【最新免费】
小编的Python的入门免费视频课程!
小编的Python的快速上手matplotlib可视化库!
崔老师爬虫实战案例免费学习视频。
陈老师数据分析报告扩展制作免费学习视频。
玩转大数据分析!Spark2.X + Python精华实战课程免费学习视频。