用Cython加速Python到“起飛”

Cython-logo

事先聲明译蒂,標(biāo)題沒有把“Python”錯打成“Cython”曼月,因?yàn)橐v的就是名為“Cython”的東西。

Cython是讓Python腳本支持C語言擴(kuò)展的編譯器柔昼,Cython能夠?qū)ython+C混合編碼的.pyx腳本轉(zhuǎn)換為C代碼十嘿,主要用于優(yōu)化Python腳本性能或Python調(diào)用C函數(shù)庫。由于Python固有的性能差的問題岳锁,用C擴(kuò)展Python成為提高Python性能常用方法绩衷,Cython算是較為常見的一種擴(kuò)展方式。

我們可以對比一下業(yè)界主流的幾種Python擴(kuò)展支持C語言的方案:


有試用版水印激率,是因?yàn)楦FT_T

ctypes是Python標(biāo)準(zhǔn)庫支持的方案咳燕,直接在Python腳本中導(dǎo)入C的.so庫進(jìn)行調(diào)用,簡單直接乒躺。swig是一個通用的讓高級腳本語言擴(kuò)展支持C的工具招盲,自然也是支持Python的。ctypes沒玩過嘉冒,不做評價曹货。以c語言程序性能為基準(zhǔn)的話咆繁,cython封裝后下降20%,swig封裝后下降70%顶籽。功能方面玩般,swig對結(jié)構(gòu)體和回調(diào)函數(shù)都要使用typemap進(jìn)行手工編寫轉(zhuǎn)換規(guī)則,typemap規(guī)則寫起來略復(fù)雜礼饱,體驗(yàn)不是很好坏为。cython在結(jié)構(gòu)體和回調(diào)上也要進(jìn)行手工編碼處理,不過比較簡單镊绪。

Cython簡單實(shí)例

我們嘗試用Cython匀伏,讓Python腳本調(diào)用C語言寫的打印“Hello World”的函數(shù),來熟悉一下Cython的玩法蝴韭。注:本文全部示例的完整代碼見gihub >>> cython_tutorials

/*filename: hello_world.h */
void print_hello_world();
/*filename: hello_world.c */
#include <stdio.h>
#include "hello_world.h"

void print_hello_world()
{
    printf("hello world...");
}

int main(int arch, char *argv[])
{
    print_hello_world();
    return (0);
}
#file: hello_world.pyx

cdef extern from "hello_world.h":
    void print_hello_world()

def cython_print_hello_world():
    print_hello_world()
#filename: Makefile
all: hello_world cython_hello_world

hello_world:
    gcc hello_world.c -c hello_world.c
    gcc hello_world.o -o hello_world 

cython:
    cython cython_hello_world.pyx

cython_hello_world: cython
    gcc cython_hello_world.c -fPIC -c
    gcc -shared -lpython2.7 -o cython_hello_world.so hello_world.o cython_hello_world.o

clean:
    rm -rf hello_world hello_world.o cython_hello_world.so cython_hello_world.c cython_hello_world.o

用Cython擴(kuò)展C够颠,最重要的就是編寫.pyx腳本文件。.pyx腳本是Python調(diào)用C的橋梁榄鉴,.pyx腳本中即能用Python語法寫履磨,也可以用類C語法寫。

$ make all    # 詳細(xì)的編譯過程可以看Makefile中的相關(guān)指令
$ python
>>> import cython_hello_world
>>> cython_hello_world.cython_print_hello_world()
hello world...
>>>

可以看到牢硅,我們成功的在Python解釋器中調(diào)用了C語言實(shí)現(xiàn)的函數(shù)。

Cython的注意事項(xiàng)

所有工具/語言的簡單使用都是令人愉快的芝雪,但是深入細(xì)節(jié)就會發(fā)現(xiàn)處處“暗藏殺機(jī)”减余。最近是項(xiàng)目需要擴(kuò)展C底層庫給Python調(diào)用,所以引入了Cython惩系。實(shí)踐過程中踩了很多坑位岔,熬了很多夜T_T。遇到了以下幾點(diǎn)需要特別注意的點(diǎn):

  1. .pyx中用cdef定義的東西堡牡,除類以外對.py都是不可見的抒抬;
  1. .py中是不能操作C類型的,如果想在.py中操作C類型就要在.pyx中從python object轉(zhuǎn)成C類型或者用含有set/get方法的C類型包裹類晤柄;
  1. 雖然Cython能對Python的str和C的“char *”之間進(jìn)行自動類型轉(zhuǎn)換擦剑,但是對于“char a[n]”這種固定長度的字符串是無法自動轉(zhuǎn)換的。需要使用Cython的libc.string.strcpy進(jìn)行顯式拷貝芥颈;
  1. 回調(diào)函數(shù)需要用函數(shù)包裹惠勒,再通過C的“void *”強(qiáng)制轉(zhuǎn)換后才能傳入C函數(shù)。

1. .pyx中用cdef定義的類型爬坑,除類以外對.py都不可見

我們來看一個例子:

#file: invisible.pyx
cdef inline cdef_function():
    print('cdef_function')

def def_function():
    print('def_function')

cdef int cdef_value

def_value = 999

cdef class cdef_class:
    def __init__(self):
        self.value = 1

class def_class:
    def __init__(self):
        self.value = 1
#file: test_visible.py
import invisible

if __name__ == '__main__':
    print('invisible.__dict__', invisible.__dict__)

輸出的invisible模塊的成員如下:

$ python invisible.py
{
'__builtins__': <module '__builtin__' (built-in)>, 
'def_class': <class invisible.def_class at 0x10feed1f0>, 
'__file__': '/git/EasonCodeShare/cython_tutorials/invisible-for-py/invisible.so', 
'call_all_in_pyx': <built-in function call_all_in_pyx>, 
'__pyx_unpickle_cdef_class': <built-in function __pyx_unpickle_cdef_class>, 
'__package__': None, 
'__test__': {}, 
'cdef_class': <type 'invisible.cdef_class'>, 
'__name__': 'invisible', 
'def_value': 999, 
'def_function': <built-in function def_function>, 
'__doc__': None}

我們在.pyx用cdef定義的函數(shù)cdef_function纠屋、變量cdef_value都看不到了,只有類cdef_class能可見盾计。所以售担,使用過程中要注意可見性問題赁遗,不要錯誤的在.py中嘗試使用不可見的模塊成員。

2. .py傳遞C結(jié)構(gòu)體類型

Cython擴(kuò)展C的能力僅限于.pyx腳本中族铆,.py腳本還是只能用純Python岩四。如果你在C中定義了一個結(jié)構(gòu),要從Python腳本中傳進(jìn)來就只能在.pyx手工轉(zhuǎn)換一次骑素,或者用包裹類傳進(jìn)來炫乓。我們來看一個例子:

/*file: person_info.h */
typedef struct person_info_t
{
    int age;
    char *gender;
}person_info;

void print_person_info(char *name, person_info *info);
//file: person_info.c
#include <stdio.h>
#include "person_info.h"

void print_person_info(char *name, person_info *info)
{
    printf("name: %s, age: %d, gender: %s\n",
            name, info->age, info->gender);
}
#file: cython_person_info.pyx
cdef extern from "person_info.h":
    struct person_info_t:
        int age
        char *gender
    ctypedef person_info_t person_info

    void print_person_info(char *name, person_info *info)

def cyprint_person_info(name, info):
    cdef person_info pinfo
    pinfo.age = info.age
    pinfo.gender = info.gender
    print_person_info(name, &pinfo)

因?yàn)椤癱yprint_person_info”的參數(shù)只能是python object,所以我們要在函數(shù)中手工編碼轉(zhuǎn)換一下類型再調(diào)用C函數(shù)献丑。

#file: test_person_info.py
from cython_person_info import cyprint_person_info

class person_info(object):
    age = None
    gender = None

if __name__ == '__main__':
    info = person_info()
    info.age = 18
    info.gender = 'male'
    
    cyprint_person_info('handsome', info)
$ python test_person_info.py
name: handsome, age: 18, gender: male

能正常調(diào)用到C函數(shù)末捣。可是创橄,這樣存在一個問題箩做,如果我們C的結(jié)構(gòu)體字段很多,我們每次從.py腳本調(diào)用C函數(shù)都要手工編碼轉(zhuǎn)換一次類型數(shù)據(jù)就會很麻煩妥畏。還有更好的一個辦法就是給C的結(jié)構(gòu)體提供一個包裹類邦邦。

#file: cython_person_info.pyx
from libc.stdlib cimport malloc, free
cdef extern from "person_info.h":
    struct person_info_t:
        int age
        char *gender
    ctypedef person_info_t person_info

    void print_person_info(char *name, person_info *info)

def cyprint_person_info(name, person_info_wrap info):
    print_person_info(name, info.ptr)


cdef class person_info_wrap(object):
    cdef person_info *ptr
    
    def __init__(self):
        self.ptr = <person_info *>malloc(sizeof(person_info))
    
    def __del__(self):
        free(self.ptr)
    
    @property
    def age(self):
        return self.ptr.age
    @age.setter
    def age(self, value):
        self.ptr.age = value
    
    @property
    def gender(self):
        return self.ptr.gender
    @gender.setter
    def gender(self, value):
        self.ptr.gender = value

我們定義了一個“person_info”結(jié)構(gòu)體的包裹類“person_info_wrap”,并提供了成員set/get方法醉蚁,這樣就可以在.py中直接賦值了燃辖。減少了在.pyx中轉(zhuǎn)換數(shù)據(jù)類型的步驟,能有效的提高性能网棍。

#file: test_person_info.py
from cython_person_info import cyprint_person_info, person_info_wrap

if __name__ == '__main__':
    info_wrap = person_info_wrap()
    info_wrap.age = 88
    info_wrap.gender = 'mmmale'
    
    cyprint_person_info('hhhandsome', info_wrap)
$ python test_person_info.py 
name: hhhandsome, age: 88, gender: mmmale

3. python的str傳遞給C固定長度字符串要用strcpy

正如在C語言中黔龟,字符串之間不能直接賦值拷貝,而要使用strcpy復(fù)制一樣滥玷,python的str和C字符串之間也要用cython封裝的libc.string.strcpy函數(shù)來拷貝氏身。我們稍微修改上一個例子,讓person_info結(jié)構(gòu)體的gender成員為16字節(jié)長的字符串:

/*file: person_info.h */
typedef struct person_info_t
{
    int age;
    char gender[16];
}person_info;
#file: cython_person_info.pyx
cdef extern from "person_info.h":
    struct person_info_t:
        int age
        char gender[16]
    ctypedef person_info_t person_info
#file: test_person_info.py
from cython_person_info import cyprint_person_info, person_info_wrap

if __name__ == '__main__':
    info_wrap = person_info_wrap()
    info_wrap.age = 88
    info_wrap.gender = 'mmmale'
    
    cyprint_person_info('hhhandsome', info_wrap)
$ make
$ python test_person_info.py 
Traceback (most recent call last):
  File "test_person_info.py", line 7, in <module>
    info_wrap.gender = 'mmmale'
  File "cython_person_info.pyx", line 39, in cython_person_info.person_info_wrap.gender.__set__
    self.ptr.gender = value
  File "stringsource", line 93, in carray.from_py.__Pyx_carray_from_py_char
IndexError: not enough values found during array assignment, expected 16, got 6

cython轉(zhuǎn)換和make時候是沒有報(bào)錯的惑畴,運(yùn)行的時候提示“IndexError: not enough values found during array assignment, expected 16, got 6”蛋欣,其實(shí)就是6字節(jié)長的“mmmale”賦值給了person_info結(jié)構(gòu)體的“char gender[16]”成員。我們用strcpy來實(shí)現(xiàn)字符串之間的拷貝就ok了如贷。

#file: cython_person_info.pyx
from libc.string cimport strcpy
…… ……
cdef class person_info_wrap(object):
    cdef person_info *ptr
    …… ……
    @property
    def gender(self):
        return self.ptr.gender
    @gender.setter
    def gender(self, value):
        strcpy(self.ptr.gender, value)
$ make
$ python test_person_info.py 
name: hhhandsome, age: 88, gender: mmmale

賦值拷貝正常陷虎,成功將“mmmale”拷貝給了結(jié)構(gòu)體的gender成員。

4. 用回調(diào)函數(shù)作為參數(shù)的C函數(shù)封裝

C中的回調(diào)函數(shù)比較特殊杠袱,用戶傳入回調(diào)函數(shù)來定制化的處理數(shù)據(jù)泻红。Cython官方提供了封裝帶有回調(diào)函數(shù)參數(shù)的例子

//file: cheesefinder.h
typedef void (*cheesefunc)(char *name, void *user_data);
void find_cheeses(cheesefunc user_func, void *user_data);
//file: cheesefinder.c
#include "cheesefinder.h"

static char *cheeses[] = {
  "cheddar",
  "camembert",
  "that runny one",
  0
};

void find_cheeses(cheesefunc user_func, void *user_data) {
  char **p = cheeses;
  while (*p) {
    user_func(*p, user_data);
    ++p;
  }
}
#file: cheese.pyx
cdef extern from "cheesefinder.h":
    ctypedef void (*cheesefunc)(char *name, void *user_data)
    void find_cheeses(cheesefunc user_func, void *user_data)

def find(f):
    find_cheeses(callback, <void*>f)

cdef void callback(char *name, void *f):
    (<object>f)(name.decode('utf-8'))
import cheese

def report_cheese(name):
    print("Found cheese: " + name)

cheese.find(report_cheese)

關(guān)鍵的步驟就是在.pyx中定義一個和C的回調(diào)函數(shù)相同的回調(diào)包裹函數(shù),如上的“cdef void callback(char *name, void *f)”霞掺。之后谊路,將.py中的函數(shù)作為參數(shù)傳遞給包裹函數(shù),并在包裹函數(shù)中轉(zhuǎn)換成函數(shù)對象進(jìn)行調(diào)用菩彬。

擴(kuò)展閱讀

更進(jìn)一步的研究Cython可以參考官方文檔和相關(guān)書籍:

版權(quán)聲明:自由轉(zhuǎn)載-非商用-非衍生-保持署名(創(chuàng)意共享3.0許可證

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缠劝,一起剝皮案震驚了整個濱河市潮梯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌惨恭,老刑警劉巖秉馏,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異脱羡,居然都是意外死亡萝究,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門锉罐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帆竹,“玉大人,你說我怎么就攤上這事脓规≡粤” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵侨舆,是天一觀的道長秒紧。 經(jīng)常有香客問我,道長挨下,這世上最難降的妖魔是什么熔恢? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮臭笆,結(jié)果婚禮上叙淌,老公的妹妹穿的比我還像新娘。我一直安慰自己耗啦,他們只是感情好凿菩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布机杜。 她就那樣靜靜地躺著帜讲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪椒拗。 梳的紋絲不亂的頭發(fā)上似将,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機(jī)與錄音蚀苛,去河邊找鬼在验。 笑死,一個胖子當(dāng)著我的面吹牛堵未,可吹牛的內(nèi)容都是我干的腋舌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼渗蟹,長吁一口氣:“原來是場噩夢啊……” “哼块饺!你這毒婦竟也來了赞辩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤授艰,失蹤者是張志新(化名)和其女友劉穎辨嗽,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體淮腾,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡糟需,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了谷朝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洲押。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖徘禁,靈堂內(nèi)的尸體忽然破棺而出诅诱,到底是詐尸還是另有隱情,我是刑警寧澤送朱,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布娘荡,位于F島的核電站,受9級特大地震影響驶沼,放射性物質(zhì)發(fā)生泄漏炮沐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一回怜、第九天 我趴在偏房一處隱蔽的房頂上張望大年。 院中可真熱鬧,春花似錦玉雾、人聲如沸翔试。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽垦缅。三九已至,卻和暖如春驹碍,著一層夾襖步出監(jiān)牢的瞬間壁涎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工志秃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留怔球,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓浮还,卻偏偏與公主長得像竟坛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容