分享

教你用 Cython 自己造轮子

 LibraryPKU 2018-06-20

“Gotham” by James Gilleard

?

作者:Nugine

专栏地址zhuanlan.zhihu.com/c_168195059


在本篇文章中,我要向你展示使用 Cython 扩展 Python 的技巧。

如果你同时有 C/C++和 Python 的编码能力,我相信你会喜欢这个的。

我们要造的轮子是一个最简单的栈的实现,用 C/C++来编写能够减小不必要的开销,带来显著的加速。

步骤

  1. 建立目录

  2. 编写 C++文件

  3. 编写 pyx 文件

  4. 直接编译

  5. 测试

1. 建立目录

首先,建立我们的工作目录。

  1. mkdir pystack

  2. cd pystack

32 位版本和 64 位版本会带来不同的问题。我的 C 库是 32 位的,所以 python 库必须也是 32 位。

使用 pipenv 指定 python 版本,并安装 Cython。

  1. pipenv --python P:\Py3.6.5\python.exe

  2. pipenv install Cython

2. 编写 C++文件

按 Python 官方文档,这里 C++必须用 C 的方式编译,所以需要加上 extern 'C'。

'c_stack.h'

  1. #include 'python.h'

  2. extern 'C'{

  3.    class C_Stack {

  4.        private:

  5.        struct Node {

  6.            PyObject* val;

  7.            Node* prev;

  8.        };

  9.        Node* tail;

  10.        public:

  11.        C_Stack();

  12.        ~C_Stack();

  13.        PyObject* peek();

  14.        void push(PyObject* val);

  15.        PyObject* pop();

  16.    };

  17. }

'c_stack.cpp'

  1. extern 'C'{

  2.    #include 'c_stack.h'

  3. }

  4. C_Stack::C_Stack() {

  5.    tail = new Node;

  6.    tail->prev = NULL;

  7.    tail->val = NULL;

  8. };

  9. C_Stack::~C_Stack() {

  10.    Node *t;

  11.    while(tail!=NULL){

  12.        t=tail;

  13.        tail=tail->prev;

  14.        delete t;

  15.    }

  16. };

  17. PyObject* C_Stack::peek() {

  18.    return tail->val;

  19. }

  20. void C_Stack::push(PyObject* val) {

  21.    Node* nt = new Node;

  22.    nt->prev = tail;

  23.    nt->val = val;

  24.    tail = nt;

  25. }

  26. PyObject* C_Stack::pop() {

  27.    Node* ot = tail;

  28.    PyObject* val = tail->val;

  29.    if (tail->prev != NULL) {

  30.        tail = tail->prev;

  31.        delete ot;

  32.    }

  33.    return val;

  34. }

最简单的栈实现,只有 push,peek,pop 三个接口,作为示例足够了。

3. 编写 pyx 文件

Cython 使用 C 与 Python 混合的语法简化了扩展 Python 的步骤。

编写起来十分简单,前提是事先了解它的语法。

'pystack.pyx'

  1. # distutils: language=c++

  2. # distutils: sources = c_stack.cpp

  3. from cpython.ref cimport PyObject,Py_INCREF,Py_DECREF

  4. cdef extern from 'c_stack.h':

  5.    cdef cppclass C_Stack:

  6.        PyObject* peek();

  7.        void push(PyObject* val);

  8.        PyObject* pop();

  9. class StackEmpty(Exception):

  10.    pass

  11. cdef class Stack:

  12.    cdef C_Stack _c_stack

  13.    cpdef object peek(self):

  14.        cdef PyObject* val

  15.        val=self._c_stack.peek()

  16.        if val==NULL:

  17.            raise StackEmpty

  18.        return val

  19.    cpdef object push(self,object val):

  20.        Py_INCREF(val);

  21.        self._c_stack.push(PyObject*>val);

  22.        return None

  23.    cpdef object pop(self):

  24.        cdef PyObject* val

  25.        val=self._c_stack.pop()

  26.        if val==NULL:

  27.            raise StackEmpty

  28.        cdef object rv=object>val;

  29.        Py_DECREF(rv)

  30.        return rv

分为四个部分:

  1. 注释指定相应的 cpp 文件.

  2. 从 CPython 导入 C 符号:PyObject,PyINCREF,PyDECREF。

  3. 从'cstack.h'导入 C 符号: CStack,以及它的接口。

  4. 将其包装为 Python 对象。

注意点:

  1. 在 C 实现中,当栈为空时,返回了空指针。Python 实现中检查空指针,并抛出异常 StackEmpty.

  2. PyObject* 和 object 并不等同,需要做类型转换。

  3. push 和 pop 时要正确操作引用计数,否则会让 Python 解释器直接崩溃。一开始不知道这个,懵逼好久,偶然间看到报错与 gc 有关,才想到引用计数的问题。

4. 直接编译

  1. pipenv run cythonize -a -i pystack.cpp

生成三个文件: pystack.cpp,pystack.html,pystack.cp36-win32.pyd

pyx 编译到 cpp,再由 C 编译器编译为 pyd。

html 是 cython 提示,指出 pyx 代码中与 python 的交互程度。

pyd 就是最终的 Python 库了。

5. 测试一下

'test.py'

  1. from pystack import *

  2. st=Stack()

  3. print(dir(st))

  4. try:

  5.    st.pop()

  6. except StackEmpty as exc:

  7.    print(repr(exc))

  8. print(type(st.pop))

  9. for i in ['1',1,[1.0],1,dict(a=1)]:

  10.    st.push(i)

  11. while True:

  12.    print(st.pop())

  13. pipenv run python test.py

  14. ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',

  15. '__ne__', '__new__', '__pyx_vtable__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', 'peek', 'pop', 'push']

  16. class 'list'>

  17. {'a': 1}

  18. 1

  19. [1.0]

  20. 1

  21. 1

  22. Traceback (most recent call last):

  23. File 'test.py', line 13, in

  24.    print(st.pop())

  25. File 'pystack.pyx', line 32, in pystack.Stack.pop

  26.    cpdef object pop(self):

  27. File 'pystack.pyx', line 36, in pystack.Stack.pop

  28.    raise StackEmpty

  29. pystack.StackEmpty

与正常 Python 对象表现相同,完美!

6. 应用

  1. pipenv run python test_polish_notation.py

  2. from operator import add, sub, mul, truediv

  3. from fractions import Fraction

  4. from pystack import Stack

  5. def main():

  6.    exp = input('exp: ')

  7.    val = eval_exp(exp)

  8.    print(f'val: {val}')

  9. op_map = {

  10.    '+': add,

  11.    '-': sub,

  12.    '*': mul,

  13.    '/': truediv

  14. }

  15. def convert(exp):

  16.    for it in reversed(exp.split(' ')):

  17.        if it in op_map:

  18.            yield True, op_map[it]

  19.        else:

  20.            yield False, Fraction(it)

  21. def eval_exp(exp):

  22.    stack = Stack()

  23.    for is_op, it in convert(exp):

  24.        if is_op:

  25.            left = stack.pop()

  26.            right = stack.pop()

  27.            stack.push(it(left, right))

  28.        else:

  29.            stack.push(it)

  30.    return stack.pop()

  31. if __name__ == '__main__':

  32.    main()

  33.    # exp: + 5 - 2 * 3 / 4 7

  34.    # val: 37/7

本篇文章展示了最简单的 Cython 造轮子技巧,希望能为即将进坑和已经进坑的同学提供一块垫脚石。

Python中文社区
全球Python中文开发者的
精神部落



Python中文社区作为一个去中心化的全球技术社区,以成为全球20万Python中文开发者的精神部落为愿景,目前覆盖各大主流媒体和协作平台,与阿里、腾讯、百度、微软、亚马逊、开源中国、CSDN等业界知名公司和技术社区建立了广泛的联系,拥有来自十多个国家和地区数万名登记会员,会员来自以公安部、工信部、清华大学、北京大学、北京邮电大学、中国人民银行、中科院、中金、华为、BAT、谷歌、微软等为代表的政府机关、科研单位、金融机构以及海内外知名公司,全平台近20万开发者关注。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多