2009-12-21 84 views
234

我想從Python應用程序調用C庫。我不想包裝整個API,只包含與我的案例相關的函數和數據類型。正如我所看到的,我有三個選擇:在Python中包裝C庫:C,Cython還是ctypes?

  1. 在C中創建一個實際的擴展模塊。可能是過度的,我也想避免學習擴展寫入的開銷。
  2. 使用Cython將C庫中的相關部分公開給Python。
  3. 在Python中使用ctypes與外部庫進行通信來完成整個事情。

我不確定2)或3)是否是更好的選擇。 3)的優勢在於​​是標準庫的一部分,所產生的代碼將是純Python –,但我不確定實際的優勢有多大。

兩種選擇都有更多的優點/缺點嗎?你推薦哪種方法?


編輯:感謝所有的答案,他們任何人都希望做同樣的事情提供了一個很好的資源。當然,這個決定仍然是針對單個案例—,沒有人「這是正確的」答案。對於我自己的情況,我可能會使用ctypes,但我也期待在其他項目中嘗試使用Cython。

由於沒有單一的真實答案,接受一個有點武斷;我選擇了FogleBird的答案,因爲它提供了對ctypes的一些很好的見解,而且它現在也是最高票數的答案。不過,我建議閱讀所有答案以獲得良好的概述。

再次感謝。

+3

在一定程度上,具體應用參與(圖書館做什麼)可能會影響方法的選擇。我們已經非常成功地使用ctypes來與供應商提供的各種硬件(例如示波器)DLL進行對話,但由於與Cython或SWIG相比額外的開銷,我不一定會選擇ctypes來與數字處理庫交談。 – 2009-12-21 21:07:06

+1

現在你有什麼你在找什麼。四個不同的答案(有人也發現了SWIG)。這意味着,現在你有4個選擇,而不是3 – 2009-12-22 06:57:19

+0

@ralu這就是我想要的:-)但嚴重的是,我沒有期望(或想)一個贊成/反對錶或一個單一的答案說:「這是你需要做」。任何有關決策的問題最好由每個可能選擇的「粉絲」回答他們的理由。然後社區投票就像我自己的工作一樣(查看論點,將它們應用到我的案例中,閱讀提供的源文件等)。長話短說:這裏有一些很好的答案。 – balpha 2009-12-22 08:10:08

回答

93

​​是您最好的選擇,因爲它可以快速完成任務,並且很高興與您一起編寫Python!

我最近用ctypes封裝了一個用於與USB芯片通信的驅動程序FTDI,它非常棒。我已經完成了這一切,並在不到一個工作日的工作。 (我只實現了我們需要的功能,大約15個功能)。

我們以前使用第三方模塊PyUSB出於同樣的目的。 PyUSB是一個實際的C/Python擴展模塊。但是PyUSB在阻止讀/寫操作時沒有釋放GIL,這對我們造成了問題。所以我使用ctypes編寫了我們自己的模塊,它在調用本機函數時釋放GIL。

有一點要注意的是,ctypes不會知道#define常量和你正在使用的庫中的東西,只有函數,所以你必須在你自己的代碼中重新定義這些常量。

這裏的代碼怎麼會找一個例子(手剪斷了,只是想向你展示它的要點):

from ctypes import * 

d2xx = WinDLL('ftd2xx') 

OK = 0 
INVALID_HANDLE = 1 
DEVICE_NOT_FOUND = 2 
DEVICE_NOT_OPENED = 3 

... 

def openEx(serial): 
    serial = create_string_buffer(serial) 
    handle = c_int() 
    if d2xx.FT_OpenEx(serial, OPEN_BY_SERIAL_NUMBER, byref(handle)) == OK: 
     return Handle(handle.value) 
    raise D2XXException 

class Handle(object): 
    def __init__(self, handle): 
     self.handle = handle 
    ... 
    def read(self, bytes): 
     buffer = create_string_buffer(bytes) 
     count = c_int() 
     if d2xx.FT_Read(self.handle, buffer, bytes, byref(count)) == OK: 
      return buffer.raw[:count.value] 
     raise D2XXException 
    def write(self, data): 
     buffer = create_string_buffer(data) 
     count = c_int() 
     bytes = len(data) 
     if d2xx.FT_Write(self.handle, buffer, bytes, byref(count)) == OK: 
      return count.value 
     raise D2XXException 

有人做了各種選項some benchmarks

如果我不得不用大量的classes/templates/etc包裝一個C++庫,我可能會更猶豫。但是ctypes可以很好地適用於Python,甚至可以在Python中使用callback

+0

我不知道你的ctypes D2XX包裝是否可用。 – joshperry 2011-10-29 23:23:28

+4

加入ctypes的讚譽,但請注意一個(無證)問題:ctypes不支持分叉。如果你使用ctypes從一個進程中分離出來,並且父進程和子進程都繼續使用ctypes,那麼你會偶然發現一個令人討厭的bug,它與使用共享內存的ctypes有關。 – 2012-04-24 12:51:00

+1

@OrenShemesh有沒有關於這個問題的進一步閱讀,你可以指點我?我想我可能對我目前正在開發的一個項目很安全,因爲我相信只有父進程使用'ctypes'(對於'pyinotify'),但我想更徹底地理解這個問題。 – zigg 2012-05-07 17:50:28

19

我會扔一個又一個在那裏:SWIG

它簡單易學,做了很多正確的事情,並支持更多的語言,因此所花的時間學習它可以是非常有用的。

如果您使用SWIG,您將創建一個新的Python擴展模塊,但SWIG會爲您完成大部分繁重工作。

92

Cython本身就是一個非常酷的工具,非常值得學習,並且令人驚訝地接近Python語法。如果您使用Numpy進行任何科學計算,那麼Cython就是要走的路,因爲它與Numpy集成以實現快速矩陣操作。

Cython是Python語言的超集。你可以拋出任何有效的Python文件,它會吐出一個有效的C程序。在這種情況下,Cython只會將Python調用映射到底層的CPython API。這可能導致50%的加速,因爲你的代碼不再被解釋。

爲了獲得一些優化,你必須開始告訴Cython關於你的代碼的其他事實,比如類型聲明。如果你足夠的瞭解它,它可以將代碼簡化爲純C語言。也就是說,Python中的for循環變成了C語言中的for循環。在這裏你將看到巨大的速度增益。你也可以在這裏鏈接到外部的C程序。

使用Cython代碼也非常簡單。我認爲這本手冊聽起來很難。你從字面上只是做:

$ cython mymodule.pyx 
$ gcc [some arguments here] mymodule.c -o mymodule.so 

,然後你可以在import mymodule Python代碼和完全忘記它編譯成C.

在任何情況下,因爲用Cython是很容易安裝並開始使用,我建議試着看看它是否適合你的需求。如果它不是你想要的工具,那不會是浪費。

+1

沒問題。關於Cython的好處是你只能學習你需要的東西。如果您只想要適度的改進,您只需編譯您的Python文件即可完成。 – carl 2009-12-22 02:54:45

+14

「你可以在其上扔任何有效的Python文件,它會吐出一個有效的C程序。」 < - 不完全是,有一些限制:http://docs.cython.org/src/userguide/limitations.html 對於大多數使用情況來說這可能不是問題,但只是想完成。 – 2011-04-08 20:36:42

+7

每個版本的問題越來越少,現在該頁面顯示「大多數問題已在0.15中解決」。 – 2012-01-23 17:07:28

16

就我個人而言,我會用C編寫一個擴展模塊。不要被Python C擴展嚇倒 - 它們一點也不難寫。這些文件非常清晰且有幫助。當我第一次在Python中編寫C擴展時,我想我花了大約一個小時才弄清楚如何編寫一個 - 沒有多少時間。

+2

你是打包C庫還是編寫新的C代碼? – FogleBird 2009-12-21 20:44:48

+0

包裝C庫。你實際上可以在這裏找到代碼:http://github.com/mdippery/lehmer – mipadi 2009-12-21 20:51:47

+1

@forivall:代碼並不是真的有用,並且有更好的隨機數生成器。我只在我的電腦上備份。 – mipadi 2012-12-18 22:06:11

9

如果您已經擁有一個定義了API的庫,我認爲​​是最好的選擇,因爲您只需要進行一些初始化,然後或多或少地按照您習慣的方式調用該庫。

我認爲當你需要新的代碼時,Cython或在C中創建擴展模塊(這不是非常困難)更有用。調用該庫並執行一些複雜耗時的任務,然後將結果傳遞給Python。

對於簡單程序,另一種方法是直接執行另一個過程(外部編譯),將結果輸出到標準輸出並使用子過程模塊調用它。有時候這是最簡單的方法。

例如,如果你犯了一個控制檯C程序工程或多或少這樣

$miCcode 10 
Result: 12345678 

你可以從你的Python

>>> import subprocess 
>>> p = subprocess.Popen(['miCcode', '10'], shell=True, stdout=subprocess.PIPE) 
>>> std_out, std_err = p.communicate() 
>>> print std_out 
Result: 12345678 

把它一點點的字符串格式化,你可以採取結果以任何你想要的方式。您還可以捕獲標準錯誤輸出,所以它非常靈活。

+0

儘管此答案沒有任何不正確之處,但是如果代碼被打開以供其他人訪問,則應該謹慎'shell = True'的子進程很容易在用戶真正獲得shell時導致某種漏洞。當開發人員是唯一的用戶時,這很好,但在世界範圍內有一大堆惱人的刺,只是在等待這樣的事情。 – Ben 2015-05-09 02:16:52

10

ctypes很棒,當你已經有一個編譯庫blob來處理(如操作系統庫)。然而,調用的開銷很大,所以如果你將大量的調用加入到庫中,並且無論如何你將會編寫C代碼(或者至少編譯它),我會說去找cython。它沒有太多的工作,並且使用生成的pyd文件會更快更麻煩。

我個人傾向於使用cython來快速加速python代碼(循環和整數比較是cython特別發光的兩個區域),並且當涉及到其他庫的代碼/包裝時,我會轉向Boost.Python。 Boost.Python可以非常挑剔的設置,但是一旦你有了它的工作,它就可以直接包裝C/C++代碼。

cython也很棒包裝numpy(我從SciPy 2009 proceedings瞭解到),但我沒有使用numpy,所以我不能對此發表評論。

123

警告:Cython核心開發者的意見未來。

我幾乎總是推薦Cython over ctypes。原因是它具有更平滑的升級路徑。如果你使用ctypes,很多事情一開始就很簡單,用純Python編寫FFI代碼當然很酷,不需要編譯,構建依賴關係等等。但是,在某些時候,您幾乎肯定會發現,您必須調用很多C庫,無論是循環還是更長的一系列相互依賴的調用,並且您都希望加快速度。這是你會注意到你不能用ctypes做到的地步。或者,當你需要回調函數並且你發現你的Python回調代碼成爲瓶頸時,你想加快它並且/或者將它移到C中。再次,你不能用ctypes做到這一點。所以你必須在那個時候切換語言,並開始重寫你的代碼的一部分,可能會將你的Python/ctypes代碼逆向工程化爲純C語言,從而破壞了用普通Python寫代碼的好處。

使用Cython,OTOH,您可以完全自由地使包裝和調用代碼儘可能薄或厚。您可以從常規Python代碼中簡單地調用您的C代碼開始,並且Cython將它們轉換爲本地C調用,無需額外的調用開銷,並且Python參數的轉換開銷極低。當你注意到在進行C庫過多昂貴的調用時,你需要更多的性能,你可以開始使用靜態類型註釋周圍的Python代碼,並讓Cython將它直接優化爲C。或者,您可以開始在Cython中重新編寫C代碼的一部分,以避免調用,並專門化和循環地修改循環。如果您需要快速回調,只需使用適當的簽名編寫函數並直接將其傳遞到C回調註冊表。再次,沒有開銷,它給你明確的C調用性能。在Cython中,如果你真的無法獲得足夠快的代碼,那麼你仍然可以考慮用C(或C++或Fortran)重寫它的真正關鍵部分,並從你的Cython代碼中自然地和本地地調用它。但是,這真的是最後的選擇,而不是唯一的選擇。

所以,ctypes很高興做簡單的事情,並迅速得到一些東西運行。但是,一旦事情開始增長,您很可能會發現您從一開始就更好地使用了Cython。

+4

+1這些都是好點,非常感謝!雖然我想知道是否只將瓶頸部分移動到Cython實際上是一個很大的開銷。但是我同意,如果你期望任何類型的性能問題,你可以從一開始就使用Cython。 – balpha 2011-04-16 17:28:23

+0

這對於使用C和Python的程序員來說是否仍然適用?在這種情況下,人們可能會爭辯說Python/ctypes是更好的選擇,因爲C循環(SIMD)的矢量化有時更直接。但除此之外,我想不出任何Cython的缺點。 – 2012-03-20 12:59:55

+0

感謝您的回答!關於Cython,我遇到的一件事情是讓構建過程正確(但這也與我以前從不編寫Python模塊有關) - 我應該先編譯它,還是將Cython源文件包含在sdist和類似問題中。我寫了一篇關於它的博客文章,以防有人遇到類似的問題/疑惑:http://martinsosic.com/development/2016/02/08/wrapping-c-library-as-python-module.html – Martinsos 2017-02-21 08:08:33

34

對於從Python應用程序調用C庫,還有cffi這是ctypes的新替代方案。它帶來了新面貌爲FFI:

  • 它處理在一個迷人的,乾淨的方式問題(而不是ctypes的
  • 它不需要寫非Python代碼(如痛飲,Cython,...)
+0

絕對是去OP *想要的*包裝。 cython聽起來非常適合自己編寫熱循環,但對於接口,cffi只是從ctypes直接升級而來的。 – 2015-10-21 10:59:05

6

有一個問題讓我使用ctypes而不是cython,而其他答案中沒有提及。

使用ctypes的結果並不依賴於你正在使用的編譯器。您可以使用或多或少的語言編寫一個庫,這些語言可以編譯爲本地共享庫。無關緊要,哪個系統,哪種語言和哪種編譯器。然而,Cython受基礎設施的限制。例如,如果你想在Windows上使用intel編譯器,使cython工作起來要麻煩得多:你應該「向cython解釋」編譯器,用這個精確的編譯器重新編譯一些東西,等等,這極大地限制了可移植性。

4

如果您的目標是Windows並選擇包裝一些專有的C++庫,那麼您很快就會發現msvcrt***.dll(Visual C++ Runtime)的不同版本略有不兼容。

這意味着你可能無法使用Cython,因爲導致wrapper.pyd是針對msvcr90.dll(Python 2.7版)msvcr100.dll(Python 3.x都有)鏈接。如果你打包的庫與不同版本的運行庫鏈接,那麼你的運氣不好。

然後爲了使事情能夠正常工作,您需要爲C++庫創建C封裝器,並將該封裝器DLL與msvcrt***.dll的版本鏈接爲C++庫。然後使用ctypes在運行時動態加載您的手動壓縮包dll。

所以有很多小細節,這是很詳細地下面的文章中描述:

「美麗的本地庫(在Python)」:http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/

+0

該文章與您使用Microsoft編譯器兼容的問題沒有任何關係。在Windows上運行Cython擴展真的不是很難。我已經能夠使用MinGW幾乎所有的東西。一個好的Python發行版雖然有幫助。 – IanH 2014-03-14 04:40:01

+1

+1提及在Windows上可能的問題(我目前也有...)。 @IanH一般來說,它並不是一般的窗口,但是如果你被一個給定的第三方lib與你的python發行版不匹配,那就太糟糕了。 – sebastian 2014-08-07 07:28:50