用 Cython 造個輪子
來自專欄 Python日常90 人贊了文章
在本篇文章中,我要向你展示使用 Cython 擴展 Python 的技巧。
如果你同時有 C/C++和 Python 的編碼能力,我相信你會喜歡這個的。
我們要造的輪子是一個最簡單的棧的實現,用 C/C++來編寫能夠減小不必要的開銷,帶來顯著的加速。
步驟
- 建立目錄
- 編寫 C++文件
- 編寫 pyx 文件
- 直接編譯
- 測試
1. 建立目錄
首先,建立我們的工作目錄。
mkdir pystackcd pystack
32 位版本和 64 位版本會帶來不同的問題。我的 C 庫是 32 位的,所以 python 庫必須也是 32 位。
使用 pipenv 指定 python 版本,並安裝 Cython。
pipenv --python P:Py3.6.5python.exepipenv install Cython
2. 編寫 C++文件
按 Python 官方文檔,這裡 C++必須用 C 的方式編譯,所以需要加上 extern "C"。
"c_stack.h"
#include "python.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"{ #include "c_stack.h"}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.cppfrom cpython.ref cimport PyObject,Py_INCREF,Py_DECREFcdef extern from c_stack.h: cdef cppclass C_Stack: PyObject* peek(); void push(PyObject* val); PyObject* pop();class StackEmpty(Exception): passcdef 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]11Traceback (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 StackEmptypystack.StackEmpty
與正常 Python 對象表現相同,完美!
6. 應用
pipenv run python test_polish_notation.pyfrom operator import add, sub, mul, truedivfrom fractions import Fractionfrom pystack import Stackdef main(): exp = input(exp: ) val = eval_exp(exp) print(fval: {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 造輪子技巧,希望能為即將進坑和已經進坑的同學提供一塊墊腳石。如果對你有所幫助,請點贊和收藏。
推薦閱讀: