教你用 Cython 自己造轮子

“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. 建立目录

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

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"

#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.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

分为四个部分:

  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. 直接编译

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"

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-china)

原文发表时间:2018-06-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员维他命

iOS 代码规范

花了一个月的时间结合几篇博客和书籍写了这套 iOS 代码规范(具体参考底部的参考文献部分)。这套代码规范除了有仅适用于 iOS 开发的部分,还有其他的比较通用性...

1872
来自专栏生信小驿站

数据处理第3部分:选择行的基本和高级的方法

原文地址:https://suzan.rbind.io/2018/02/dplyr-tutorial-3/ 作者:Suzan Baert 这是系列dplyr...

891
来自专栏FreeBuf

浅析ReDoS的原理与实践

*本文原创作者:MyKings,本文属FreeBuf原创奖励计划,未经许可禁止转载 ReDoS(Regular expression Denial of Ser...

5925
来自专栏Java成长之路

八、java对象和方法区的垃圾回收

即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性...

1362
来自专栏Linyb极客之路

编码习惯之工具类规范

一个项目不可能没有工具类,工具类的初衷是良好的,代码重用,但到了后面工具类越来越乱,有些项目工具类有几十个,看的眼花缭乱,还有不少重复。如何编写出好的工具类,我...

4049
来自专栏社区的朋友们

iOS 中的 Promise 设计模式

无论是代理模式,还是闭包,在处理单一任务的时候,都出色的完成了任务。可是当两种模式要相互配合,一起完成一系列任务,并且每个任务之间还要共享信息,相互衔接,雇主就...

2.1K1
来自专栏mini188

java中的锁

java中有哪些锁 这个问题在我看了一遍<java并发编程>后尽然无法回答,说明自己对于锁的概念了解的不够。于是再次翻看了一下书里的内容,突然有点打开脑门的感觉...

3249
来自专栏黄Java的地盘

[翻译]WebSocket协议第二章——Conformance Requirements

本文为WebSocket协议的第二章,本文翻译的主要内容为WebSocket协议中相关术语的介绍。

841
来自专栏高性能服务器开发

【java多线程】多线程并发同步问题及解决方法

一、线程并发同步概念 线程同步其核心就在于一个“同”。所谓“同”就是协同、协助、配合,“同步”就是协同步调昨,也就是按照预定的先后顺序进行运行,即“你先,我等,...

4908
来自专栏jeremy的技术点滴

JVM的Finalization Delay引起的OOM

3808

扫码关注云+社区