教你用 Cython 自己造轮子

196 阅读5分钟

640?wx_fmt=jpeg

“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. ``
  3. extern "C"{
  4.    class C_Stack {
  5.        private:
  6.        struct Node {
  7.            PyObject* val;
  8.            Node* prev;
  9.        };
  10.        Node* tail;
  11. ``
  12.        public:
  13.        C_Stack();
  14. ``
  15.        ~C_Stack();
  16. ``
  17.        PyObject* peek();
  18. ``
  19.        void push(PyObject* val);
  20. ``
  21.        PyObject* pop();
  22.    };
  23. }

"c_stack.cpp"

  1. extern "C"{
  2.    #include "c_stack.h"
  3. }
  4. ``
  5. C_Stack::C_Stack() {
  6.    tail = new Node;
  7.    tail->prev = NULL;
  8.    tail->val = NULL;
  9. };
  10. ``
  11. C_Stack::~C_Stack() {
  12.    Node *t;
  13.    while(tail!=NULL){
  14.        t=tail;
  15.        tail=tail->prev;
  16.        delete t;
  17.    }
  18. };
  19. ``
  20. PyObject* C_Stack::peek() {
  21.    return tail->val;
  22. }
  23. ``
  24. void C_Stack::push(PyObject* val) {
  25.    Node* nt = new Node;
  26.    nt->prev = tail;
  27.    nt->val = val;
  28.    tail = nt;
  29. }
  30. ``
  31. PyObject* C_Stack::pop() {
  32.    Node* ot = tail;
  33.    PyObject* val = tail->val;
  34.    if (tail->prev != NULL) {
  35.        tail = tail->prev;
  36.        delete ot;
  37.    }
  38.    return val;
  39. }

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

3. 编写 pyx 文件

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

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

"pystack.pyx"

  1. # distutils: language=c++
  2. # distutils: sources = c_stack.cpp
  3. ``
  4. from cpython.ref cimport PyObject,Py_INCREF,Py_DECREF
  5. ``
  6. cdef extern from 'c_stack.h':
  7.    cdef cppclass C_Stack:
  8.        PyObject* peek();
  9. ``
  10.        void push(PyObject* val);
  11. ``
  12.        PyObject* pop();
  13. ``
  14. class StackEmpty(Exception):
  15.    pass
  16. ``
  17. cdef class Stack:
  18.    cdef C_Stack _c_stack
  19. ``
  20.    cpdef object peek(self):
  21.        cdef PyObject* val
  22.        val=self._c_stack.peek()
  23.        if val==NULL:
  24.            raise StackEmpty
  25.        return <object>val
  26. ``
  27.    cpdef object push(self,object val):
  28.        Py_INCREF(val);
  29.        self._c_stack.push(<PyObject*>val);
  30.        return None
  31. ``
  32.    cpdef object pop(self):
  33.        cdef PyObject* val
  34.        val=self._c_stack.pop()
  35.        if val==NULL:
  36.            raise StackEmpty
  37.        cdef object rv=<object>val;
  38.        Py_DECREF(rv)
  39.        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. ``
  9. print(type(st.pop))
  10. for i in ['1',1,[1.0],1,dict(a=1)]:
  11.    st.push(i)
  12. while True:
  13.    print(st.pop())
  14. ``
  15. ``
  16. pipenv run python test.py
  17. ``
  18. ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__',
  19. '__ne__', '__new__', '__pyx_vtable__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', 'peek', 'pop', 'push']
  20. ``
  21. <class 'list'>
  22. {'a': 1}
  23. 1
  24. [1.0]
  25. 1
  26. 1
  27. Traceback (most recent call last):
  28. File "test.py", line 13, in <module>
  29.    print(st.pop())
  30. File "pystack.pyx", line 32, in pystack.Stack.pop
  31.    cpdef object pop(self):
  32. File "pystack.pyx", line 36, in pystack.Stack.pop
  33.    raise StackEmpty
  34. pystack.StackEmpty

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

6. 应用

  1. pipenv run python test_polish_notation.py
  2. ``
  3. from operator import add, sub, mul, truediv
  4. from fractions import Fraction
  5. from pystack import Stack
  6. ``
  7. def main():
  8.    exp = input('exp: ')
  9.    val = eval_exp(exp)
  10.    print(f'val: {val}')
  11. ``
  12. ``
  13. op_map = {
  14.    '+': add,
  15.    '-': sub,
  16.    '*': mul,
  17.    '/': truediv
  18. }
  19. ``
  20. ``
  21. def convert(exp):
  22.    for it in reversed(exp.split(' ')):
  23.        if it in op_map:
  24.            yield True, op_map[it]
  25.        else:
  26.            yield False, Fraction(it)
  27. ``
  28. ``
  29. def eval_exp(exp):
  30.    stack = Stack()
  31. ``
  32.    for is_op, it in convert(exp):
  33.        if is_op:
  34.            left = stack.pop()
  35.            right = stack.pop()
  36.            stack.push(it(left, right))
  37.        else:
  38.            stack.push(it)
  39.    return stack.pop()
  40. ``
  41. ``
  42. if __name__ == '__main__':
  43.    main()
  44.    # exp: + 5 - 2 * 3 / 4 7
  45.    # val: 37/7

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

640?wx_fmt=jpegPython中文社区 全球Python中文开发者的 精神部落 640?wx_fmt=jpeg.jpg")

\

640?wx_fmt=gif

\

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

640?wx_fmt=jpeg

点击下方****阅读原文 免费成为****社区会员