好好的為何要混合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。
第1章 Cython的安裝和使用
1.1 安裝
在Linux下通過pip install Cython
安裝。安裝完畢后執行cython --version
,如果輸出了版本號即安裝成功。
1.2 快速入門
本節完整代碼見這里
安裝完成后,我們創建一個Hello World項目,需要創建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實現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項目;
主要步驟是:
- 使用
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可以指一段代碼、一個類,甚至也能泛指一類技術。
就和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.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的皮毛,若有疑問歡迎交流。