在做複雜計算時,Python的執行效率是無法同C比的。而且有些演算法已經有開源的C庫了,我們也沒必要用Python重寫一份。那Python怎麼呼叫C語言寫的程式庫呢?答案就在Python提供的ctypes庫,它提供同C語言相容的資料型別,可以很方便地呼叫C語言動態連結庫中的函式。
使用C標準庫函式
我們來試試呼叫C標準庫函式:
1 2 3 4 5 6 7 |
from ctypes import cdll libc = cdll.LoadLibrary('libc.so.6') # Load standard C library on Linux # libc = cdll.LoadLibrary('libc.dylib') # Load standard C library on Mac # libc = cdll.msvcrt # Load standard C library on Windows print libc.time(None) |
上述程式碼載入了Linux中的C標準庫”libc.so.6″,並呼叫其中”time()”函式,執行後螢幕上會列印出當前時間戳。注,Windows和Mac上的載入方法在註釋中。
呼叫自定義的動態連結庫
我們先根據這篇文章寫個動態連結庫,現在你有了庫”libhello.so”,其有一個hello函式。讓我們在Python中呼叫它:
1 2 3 4 |
from ctypes import cdll libhello= cdll.LoadLibrary("./libhello.so") libhello.hello('You') |
看到螢幕上”Hello You!”的字樣了吧。對!就這麼簡單,比起Java呼叫原生程式碼方便很多吧。注意,本例中的”libhello.so”同Python程式在同一目錄下。
效率對比
我們寫個階乘(factorial)函式,來比較Python和C的執行效率吧。先寫C程式碼:
1 2 3 4 5 6 |
int factorial(int n) { if (n < 2) return 1; return factorial(n - 1) * n; } |
方便起見,我們把它放在之前寫的”hello.c”檔案中,這樣就可以從”libhello.so”中呼叫它。別忘了在”hello.h”中宣告這個函式。然後實現Python程式碼:
1 2 3 4 5 6 7 |
def factorial(n): if n < 2: return 1 return factorial(n - 1) * n def factorial_c(n): return libhello.factorial(n) |
Python的實現可以說同C的一模一樣,我們另外定義一個”factorial_c()”函式來封裝C的呼叫。現在,我們來比較下執行效率。這裡要引入Python的”timeit”包,它可以幫你計算程式的執行時間,省去你很多程式碼。讓我們來算20的階乘,並計算10萬次,看看所消耗的時間:
1 2 3 4 5 6 |
from timeit import timeit f_setup = 'from __main__ import factorial' f_c_setup = 'from __main__ import factorial_c' print timeit('factorial(20)', setup=f_setup, number=100000) print timeit('factorial_c(20)', setup=f_c_setup, number=100000) |
我在虛擬機器上跑的結果結果是:
1 2 |
0.231598138809 0.0475780963898 |
差不多5倍的差距啊!
引數傳址
大家知道C的函式引數是傳值的(其實Python也一樣),那我想在C中改變引數的值怎麼辦,那就需要傳遞引用了。我們在上面的”libhello.so”中加一個快排函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void quickSort(int *a, int start, int end) { if (start < end) { int left = start; int right = end; int key = a[left]; while (left < right) { while (left < right && a[right] >= key) right--; a[left] = a[right]; while (left < right && a[left] <= key) left++; a[right] = a[left]; } a[left] = key; quickSort(a, start, right-1); quickSort(a, left + 1, end); } } |
朋友們馬上可以看出,這段函式中陣列a中的值是可以被改變的。那Python怎麼呼叫它呢?就是在引數傳遞時,加上”byref()”呼叫,它是ctypes提供的方法,如果用它呼叫int型變數a時,作用類似於”(int *) &a”。所以我們的Python程式可以這樣寫:
1 2 3 4 5 6 7 |
from ctypes import cdll, c_int, byref def quick_sort(numbers): size = len(numbers) c_numbers = (c_int * size)(*numbers) libhello.quickSort(byref(c_numbers), 0, size) return c_numbers |
這裡還有個知識點,就是C型別。為了同C的變數型別相容,ctypes庫提供了一系列對應的C型別。本例中c_int就是對應C中的int型。我們將”c_int * 10″就等於建立一個長度為10的int型陣列。而後面的(*number)就是把numbers的值賦給剛建立的int陣列。ctypes庫所有提供的C型別可以在這裡查到。
上例中,我們必須傳入C型別的整型陣列才能被C程式接收。現在讓我們來使用下這個快排:
1 2 3 4 5 6 7 |
from random import shuffle, sample numbers = sample(range(1000), 99) shuffle(numbers) sorted_num = quick_sort(numbers) for i in sorted_num: print i |
有興致的朋友們也可以寫個Python的快排來比較下效率。
引數及返回型別指定
我們回到C標準庫,呼叫下”strchr”方法,它的作用是在字串中找出以指定字元開頭的子串。
1 2 |
strchr = libc.strchr print strchr('abcdef', 'd') |
你會發現,返回一直是0,而我們期望的應該是”def”。其實,問題是在我們的第二個引數,它應該是一個字元,而Python中它是一個字串。那怎麼讓它成為字元型別呢?一個方法是使用”strchr(‘abcdef’, ord(‘d’))”呼叫”strchr”方法,”ord()”函式可以把字串變成字元型別,但是每次呼叫都要加上,很麻煩。還有一個辦法就是指定函式輸入引數的型別。我們可以加上程式碼:
1 2 3 4 5 |
from ctypes import c_char, c_char_p strchr = libc.strchr strchr.argtypes = [c_char_p, c_char] print strchr('abcdef', 'd') |
函式的”argtypes”屬性就可以指定傳入引數的型別。這裡,第一個引數是字元指標,也就是C中的字串,第二個是字元。
我們再來執行下程式,奇怪,雖然有返回了,但一直是一個長整型數值,為什麼呢?瞭解’strchr’的朋友們應該知道,這個函式返回的是”char *”型別,它是一個字元指標,所以你在Python中獲取的那個數值,就是指標的地址。那要怎麼把指標轉為字串呢?也很簡單,通過函式的”restype”屬性指定返回值型別即可。完整的程式如下:
1 2 3 4 5 6 7 8 9 10 |
from ctypes import cdll, c_char, c_char_p libc = cdll.LoadLibrary('libc.so.6') # Load standard C library on Linux strchr = libc.strchr strchr.argtypes = [c_char_p, c_char] strchr.restype = c_char_p substr = strchr('abcdef', 'd') if substr: print substr |
關於ctypes庫的更詳細內容可以參考Python官方文件。
文中的示例程式碼可以從這裡下載