
好好的為何要混合Python代碼和C代碼呢?原因主要有2個:
- Python性能差,將一部分核心邏輯用C語言實現(xiàn)以提升整體性能
- 希望Python能夠調(diào)用一個C語言實現(xiàn)的系統(tǒng),典型例子:OpenCV計算機視覺庫
Python、C混合編程并不奇怪,Python官方就提供了Python/C API可以實現(xiàn)「用C語言編寫Python庫」,見官方文檔,如果你點開看了你可能就會發(fā)現(xiàn),這好難?。ython/C API入門門檻太高,于是有了Cython的誕生。
Cython是基于Python/C API的,但學習Cython的時候完全不用了解Python/C API。

第1章 Cython的安裝和使用
1.1 安裝
在Linux下通過pip install Cython安裝。安裝完畢后執(zhí)行cython --version,如果輸出了版本號即安裝成功。
1.2 快速入門
本節(jié)完整代碼見這里
安裝完成后,我們創(chuàng)建一個Hello World項目,需要創(chuàng)建hello.pyx和setup.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實現(xiàn)Python調(diào)用C庫
完整代碼見這里
如果我們已經(jīng)有一個C語言的動態(tài)庫、靜態(tài)庫,如何在Python中調(diào)用外部C庫呢(本節(jié)以動態(tài)庫為例)?
現(xiàn)有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編譯
本節(jié)完整代碼見這里
本節(jié)介紹gcc這種比較原始的編譯方式,是希望你能搞懂Cython如何運作。如果能掌握那么相信在日后的開發(fā)工作中各種編譯、部署的問題都不太可能難倒你。
我們知道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個哭臉,不但代碼寫起來煩人,編譯構(gòu)建也煩人,所以我們才用Cython取代Python/C API;
- 第二條線是我們最常用的setup.py,有2個笑臉,Cython項目最常用的方式;
- 第三條線有1個哭臉,也是本節(jié)要講的,如何使用gcc這種傳統(tǒng)的方式來編譯Cython項目;

主要步驟是:
- 使用
cython xxx.pyx生成xxx.c - 然后使用
gcc -fPIC -shared -I/usr/include/python2.7/ xxx.c -o xxx.so來生成so文件 - 要注意頭文件版本,自己用的是python2的頭文件還是python3的頭文件
第2章 Cython封裝C庫基礎(chǔ)
2.1 在Cython中調(diào)用C庫函數(shù)
本節(jié)完整代碼見這里
C語言有很多庫函數(shù),例如:
- libc的
atoi函數(shù) - math庫的
sin函數(shù)
這些庫函數(shù)非常常用,所以Cython已經(jīng)幫我們封裝了,所以我們直接調(diào)用即可。
那么Cython到底幫我們封裝了多少C庫函數(shù)呢?你可以在這里找找。
如果你需要調(diào)用的函數(shù)Cython沒有封裝,那么你需要自己封裝,會在2.2節(jié)介紹。
現(xiàn)在我們看下Cython如何調(diào)用這些封裝好的C庫函數(shù):
# 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 實現(xiàn)Python環(huán)境調(diào)用C庫函數(shù)
本節(jié)完整代碼見這里。
在2.1節(jié)我們已經(jīng)看到Cython能夠調(diào)用C函數(shù),Cython中定義的函數(shù)能被Python調(diào)用,因此Cython就成為了Python調(diào)用C的“橋梁”,我們把這一過程叫做wrap,實現(xiàn)這一功能的Cython代碼叫做wrapper,見圖2-1。通常wrapper可以指一段代碼、一個類,甚至也能泛指一類技術(shù)。

就和C語言開發(fā)一樣,Cython代碼也需要:包含頭文件、鏈接靜態(tài)庫/動態(tài)庫。
對于這幾個C結(jié)構(gòu)體、函數(shù):
// 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中調(diào)用:
# 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中完全可以調(diào)用C函數(shù)
cdef Queue *q
q = queue_new()
queue_free(q)
上面代碼看出來雖然Cython可以調(diào)用C,但作為wrapper還有一個要求是將C語言自然地封裝成Python風格,所以還需要下面這段代碼讓API更加符合面向?qū)ο螅?/p>
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])
)
這里只貼了創(chuàng)建、釋放的封裝。其它功能(如pop、push)見完整代碼。
2.3 回調(diào)函數(shù)
本節(jié)完整代碼見這里。
對于一些需要傳入回調(diào)函數(shù)的接口,會造成調(diào)用、被調(diào)用關(guān)系的反轉(zhuǎn)。在之前我們討論的都是在Cython中調(diào)用C函數(shù),然而回調(diào)函數(shù)使得問題變?yōu)槿绾巫孋調(diào)用Cython函數(shù)。例如現(xiàn)在希望封裝一個這樣的C函數(shù):
void traverse(int *arr, int len, void (*cb)(int)) {
for (int i = 0; i < len; i++) {
cb(arr[i]);
}
}
為了實現(xiàn)回調(diào)的封裝:
- 首先需要在Cython中定義一個能被C語言調(diào)用的
wrap_cb,這是容易的 - 然后需要在Cython的
wrap_cb中調(diào)用Python的回調(diào)函數(shù)(我們把它叫做app_cb),這步會比較難實現(xiàn),因為C環(huán)境調(diào)用wrap_cb時無法將app_cb的信息傳入
在圖2-2展示的方案中,將app_cb存至全局變量,這樣wrap_cb可以從全局變量取到app_cb。

2.4 異步回調(diào)
2.3節(jié)中提到的方案不適用于異步場景,見下文專門章節(jié)分析異步場景。
2.5 結(jié)構(gòu)體的封裝
本節(jié)完整代碼見這里。
第3章 pxd文件
就像C語言有.c和.h文件,Cython有.pyx和.pxd文件,可以幫助更好的組織、管理代碼,pxd也可以實現(xiàn)wrapper的復用。
3.1 名稱沖突問題
本節(jié)完整代碼見這里
在之前的例子中,我們把C函數(shù)的導入、Python wrapper的封裝都放在了pyx文件中,這會導致一些符號名沖突。例如:
cdef extern from "queue.h":
# 這是聲明C語言中有一個名為Queue的結(jié)構(gòu)體
ctypedef struct Queue:
pass
# 這是提供給Python用的類,我們其實也想起名叫做Queue,但C語言結(jié)構(gòu)體也叫這個名字
# 所以我們不得不把提供給Python的類名改為PyQueue
cdef class PyQueue:
cdef Queue *_c_queue
def __cinit__(self):
self._c_queue = ...
為了解決開發(fā)中遇到的這些問題,我們可以把聲明放在pxd中,這樣就多了一層命名空間,如下:
# cqueue.pxd
cdef extern from "queue.h":
ctypedef struct Queue:
pass
有了命名空間,在pyx中就不會產(chǎn)生符號名沖突了:
# queue.pyx
cimport cqueue
cdef class Queue:
cdef cqueue.Queue *_c_queue
def __cinit__(self):
self._c_queue = ...
3.2 Cython代碼復用
第4章 異步和內(nèi)存管理
C程序員手動管理內(nèi)存,而Python得益于垃圾回收機制,程序員無需感知內(nèi)存管理。
附錄:Cython語法參考
Cython易用的原因是它的代碼跟Python幾乎一樣,Cython的語法是Python的「超集」,即Python代碼一定是Cython代碼,而Cython代碼不一定是Python代碼。比起Python來說,Cython多了一些跟C語言相關(guān)的語法。
# 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函數(shù)
c = 0 # 創(chuàng)建Python的int對象
c = a + b
return c
# Cython語法
cdef int foo(int a, int b): # cdef是定義C語言函數(shù),注意該函數(shù)不能被Python調(diào)用
cdef int c = 0 # 這是C語言的int變量
c = a + b
return c # 返回C語言的int
# Cython語法
cpdef int foo(int a, int b): # cpdef定義的函數(shù)可以被Python調(diào)用
cdef int c = 0 # C語言的int變量
c = a + b
# 返回的是Python的int對象
# Cython在這里隱式將C語言int變量轉(zhuǎn)為了Python的int對象
# 因為變量c是基本類型,Cython幫忙轉(zhuǎn)了,如果c是復雜的是不能直接return的
return c
# Python語法
class Person():
def __init__(self): # 這是構(gòu)造函數(shù)
pass
# Cython語法
class Person():
def __init__(self): # 和C語言相關(guān)的內(nèi)存分配(如malloc)不能放在這里實現(xiàn)
pass
def __cinit__(self): # 和C語言相關(guān)的內(nèi)存分配(如malloc)要放在這里實現(xiàn)
... = malloc();
def __dealloc__(self): # 和C語言相關(guān)的內(nèi)存釋放(如free)要放在這里實現(xiàn)
free(...);
寫在最后:完整介紹Cython是一個龐大的工程,本文只是介紹了Cython的皮毛,若有疑問歡迎交流。