教你用 Cython 自己造轮子

教你用 Cython 自己造轮子_第1张图片

“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

  31. 分为四个部分:

    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 造轮子技巧,希望能为即将进坑和已经进坑的同学提供一块垫脚石。

    教你用 Cython 自己造轮子_第2张图片Python中文社区 全球Python中文开发者的 精神部落 教你用 Cython 自己造轮子_第3张图片


    640?wx_fmt=gif


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

    教你用 Cython 自己造轮子_第4张图片

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

    你可能感兴趣的:(教你用 Cython 自己造轮子)