Cython入门教程

Cython Logo

好好的为何要混合Python代码和C代码呢?原因主要有2个:

  • Python性能差,将一部分核心逻辑用C语言实现以提升整体性能
  • 希望Python能够调用一个C语言实现的系统,典型例子:OpenCV计算机视觉库

Python、C混合编程并不奇怪,Python官方就提供了Python/C API可以实现「用C语言编写Python库」,见官方文档,如果你点开看了你可能就会发现,这好难啊!Python/C API入门门槛太高,于是有了Cython的诞生。

Cython是基于Python/C API的,但学习Cython的时候完全不用了解Python/C API。


Cython和Python/C API

第1章 Cython的安装和使用

1.1 安装

在Linux下通过pip install Cython安装。安装完毕后执行cython --version,如果输出了版本号即安装成功。

1.2 快速入门

本节完整代码见这里

安装完成后,我们创建一个Hello World项目,需要创建hello.pyxsetup.py两个文件。

# file: hello.pyx
def say_hello_to(name):
    print("Hello %s!" % name)
# file: setup.py
from distutils.core import setup
from Cython.Build import cythonize

setup(name='Hello world app',
      ext_modules=cythonize("hello.pyx"))

这样编译项目:python setup.py build_ext --inplace,会生成hello.so以及一些没用的中间文件。
下面测试我们生成的hello.so能不能用:

# coding: utf-8
# 这个import会先找hello.py,找不到就会找hello.so
import hello  # 导入了hello.so

hello.say_hello_to('张三')

1.3 Cython实现Python调用C库

完整代码见这里

如果我们已经有一个C语言的动态库、静态库,如何在Python中调用外部C库呢(本节以动态库为例)?

现有C库如下,是一个叫做cmath的库:

// file: cmath.c
#include "cmath.h"
int add(int a, int b)
{
    return a + b;
}
// file: cmath.h
int add(int a, int b);

下面将该cmath封装为Python库,为了防止名称冲突,命名为pymath:

# file: pymath.pyx
cdef extern from "cmath.h":
    int add(int a, int b)

def pyadd(int a, int b):
    return add(a, b)

然后还需要写setup.py,但这里不想写setup.py了,因为本文主要使用gcc手工编译的方式。

1.4 手工gcc编译

本节完整代码见这里

本节介绍gcc这种比较原始的编译方式,是希望你能搞懂Cython如何运作。如果能掌握那么相信在日后的开发工作中各种编译、部署的问题都不太可能难倒你。

我们知道Ubuntu下Python是这样安装的:apt-get install python3,但你可能不知道有这个东西:apt-get install python3-dev
python3-dev这个包安装的是Python的头文件,以Ubuntu 18.04为例,安装完成后你应该可以在/usr/include/python3.6/找到一些头文件。

看图1-1可以看到3种方式的对比:

  • 第一条线是用Python/C API,有2个哭脸,不但代码写起来烦人,编译构建也烦人,所以我们才用Cython取代Python/C API;
  • 第二条线是我们最常用的setup.py,有2个笑脸,Cython项目最常用的方式;
  • 第三条线有1个哭脸,也是本节要讲的,如何使用gcc这种传统的方式来编译Cython项目;
图1-1 3种方式对比

主要步骤是:

  • 使用cython xxx.pyx生成xxx.c
  • 然后使用gcc -fPIC -shared -I/usr/include/python2.7/ xxx.c -o xxx.so来生成so文件
  • 要注意头文件版本,自己用的是python2的头文件还是python3的头文件

第2章 Cython封装C库基础

2.1 在Cython中调用C库函数

本节完整代码见这里

C语言有很多库函数,例如:

  • libc的atoi函数
  • math库的sin函数

这些库函数非常常用,所以Cython已经帮我们封装了,所以我们直接调用即可。
那么Cython到底帮我们封装了多少C库函数呢?你可以在这里找找。
如果你需要调用的函数Cython没有封装,那么你需要自己封装,会在2.2节介绍。

现在我们看下Cython如何调用这些封装好的C库函数:

# file: demo.pyx
from libc.math cimport sin
from libc.stdlib cimport atof

def foo(char *s):
    x = atof(s)
    return sin(x)

测试一下可不可以用:

# file: test.py
import demo
print(demo.foo("3.1415"))  # 答案约等于0

2.2 实现Python环境调用C库函数

本节完整代码见这里

在2.1节我们已经看到Cython能够调用C函数,Cython中定义的函数能被Python调用,因此Cython就成为了Python调用C的“桥梁”,我们把这一过程叫做wrap,实现这一功能的Cython代码叫做wrapper,见图2-1。通常wrapper可以指一段代码、一个类,甚至也能泛指一类技术。

图2-1 wrapper

就和C语言开发一样,Cython代码也需要:包含头文件、链接静态库/动态库。

对于这几个C结构体、函数:

// file: queue.h
typedef struct _Queue Queue;
typedef void *QueueValue;
struct _Queue {
    QueueEntry *head;
    QueueEntry *tail;
};
Queue *queue_new(void);
void queue_free(Queue *queue);

希望在Cython中调用:

# file: queue.pyx
cdef extern from "queue.h":  # 包含头文件
    ctypedef struct Queue:
        pass
    ctypedef void *QueueValue

    Queue *queue_new()
    void queue_free(Queue *queue)

def foo():
    # 虽然没有实际意义,但这段代码很自嗨,可以看到Cython中完全可以调用C函数
    cdef Queue *q
    q = queue_new()
    queue_free(q)

上面代码看出来虽然Cython可以调用C,但作为wrapper还有一个要求是将C语言自然地封装成Python风格,所以还需要下面这段代码让API更加符合面向对象:

cdef class PyQueue:
    cdef Queue *_c_queue

    def __cinit__(self):
        self._c_queue = queue_new()

    def __dealloc__(self):
        if self._c_queue is not NULL:
            queue_free(self._c_queue)

编译:

# file: setup.py
from distutils.core import setup, Extension
from Cython.Build import cythonize

extension = Extension(
    "queue",
    ["queue.pyx"],
    libraries=["cqueue"]  # 在这边声明需要链接的C库(libcqueue.so)
)

setup(
    ext_modules=cythonize([extension])
)

这里只贴了创建、释放的封装。其它功能(如pop、push)见完整代码。

2.3 回调函数

本节完整代码见这里

对于一些需要传入回调函数的接口,会造成调用、被调用关系的反转。在之前我们讨论的都是在Cython中调用C函数,然而回调函数使得问题变为如何让C调用Cython函数。例如现在希望封装一个这样的C函数:

void traverse(int *arr, int len, void (*cb)(int)) {
    for (int i = 0; i < len; i++) {
        cb(arr[i]);
    }
}

为了实现回调的封装:

  • 首先需要在Cython中定义一个能被C语言调用的wrap_cb,这是容易的
  • 然后需要在Cython的wrap_cb中调用Python的回调函数(我们把它叫做app_cb),这步会比较难实现,因为C环境调用wrap_cb时无法将app_cb的信息传入

在图2-2展示的方案中,将app_cb存至全局变量,这样wrap_cb可以从全局变量取到app_cb

图2-2 回调函数的封装

2.4 异步回调

2.3节中提到的方案不适用于异步场景,见下文专门章节分析异步场景。

2.5 结构体的封装

本节完整代码见这里

第3章 pxd文件

就像C语言有.c.h文件,Cython有.pyx.pxd文件,可以帮助更好的组织、管理代码,pxd也可以实现wrapper的复用。

3.1 名称冲突问题

本节完整代码见这里

在之前的例子中,我们把C函数的导入、Python wrapper的封装都放在了pyx文件中,这会导致一些符号名冲突。例如:

cdef extern from "queue.h":
    # 这是声明C语言中有一个名为Queue的结构体
    ctypedef struct Queue:
        pass

# 这是提供给Python用的类,我们其实也想起名叫做Queue,但C语言结构体也叫这个名字
# 所以我们不得不把提供给Python的类名改为PyQueue
cdef class PyQueue:
    cdef Queue *_c_queue

    def __cinit__(self):
        self._c_queue = ...

为了解决开发中遇到的这些问题,我们可以把声明放在pxd中,这样就多了一层命名空间,如下:

# cqueue.pxd
cdef extern from "queue.h":
    ctypedef struct Queue:
        pass

有了命名空间,在pyx中就不会产生符号名冲突了:

# queue.pyx
cimport cqueue
cdef class Queue:
    cdef cqueue.Queue *_c_queue

    def __cinit__(self):
        self._c_queue = ...

3.2 Cython代码复用

第4章 异步和内存管理

C程序员手动管理内存,而Python得益于垃圾回收机制,程序员无需感知内存管理。

附录:Cython语法参考

Cython易用的原因是它的代码跟Python几乎一样,Cython的语法是Python的「超集」,即Python代码一定是Cython代码,而Cython代码不一定是Python代码。比起Python来说,Cython多了一些跟C语言相关的语法。

# Python语法
import math  # 导入math.py或math.so或math目录
from math import add as myadd  # Python:导入math.py中的add符号,为避免名字冲突,重命名为myadd
math.add(1, 2)  # 访问math中的add符号
myadd(1, 2)

# 对应的Cython语法
cimport math  # 导入math.pxd
from math cimport add as myadd  # 导入math.pxd中的add符号,为避免名字冲突,重命名为myadd
math.add(1, 2)  # 访问math中的add符号
myadd(1, 2)
# Python语法
def foo(a, b):  # 定义foo函数
    c = 0  # 创建Python的int对象
    c = a + b
    return c

# Cython语法
cdef int foo(int a, int b):  # cdef是定义C语言函数,注意该函数不能被Python调用
    cdef int c = 0  # 这是C语言的int变量
    c = a + b
    return c  # 返回C语言的int

# Cython语法
cpdef int foo(int a, int b):  # cpdef定义的函数可以被Python调用
    cdef int c = 0  # C语言的int变量
    c = a + b

    # 返回的是Python的int对象
    # Cython在这里隐式将C语言int变量转为了Python的int对象
    # 因为变量c是基本类型,Cython帮忙转了,如果c是复杂的是不能直接return的
    return c
# Python语法
class Person():
    def __init__(self):  # 这是构造函数
        pass

# Cython语法
class Person():
    def __init__(self):  # 和C语言相关的内存分配(如malloc)不能放在这里实现
        pass

    def __cinit__(self):  # 和C语言相关的内存分配(如malloc)要放在这里实现 
        ... = malloc();

    def __dealloc__(self):  # 和C语言相关的内存释放(如free)要放在这里实现 
        free(...);

写在最后:完整介绍Cython是一个庞大的工程,本文只是介绍了Cython的皮毛,若有疑问欢迎交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容