用Python包装C库:C,Cython还是ctypes?

我想从Python应用程序调用一个C库。 我不想包装整个API,只包含与我的情况相关的函数和数据types。 正如我所看到的,我有三个select:

  1. 在C中创build一个实际的扩展模块可能是过度的,我也想避免学习扩展写作的开销。
  2. 使用Cython将C库中的相关部分公开给Python。
  3. 在Python中完成整个事情,使用ctypes与外部库进行通信。

我不确定2)还是3)是更好的select。 3)的优点是ctypes是标准库的一部分,所产生的代码将是纯Python–尽pipe我不确定这个优点实际上有多大。

这两种select是否有更多的优点/缺点? 你推荐哪种方法?


编辑:谢谢你所有的答案,他们提供了一个很好的资源,任何人想要做类似的事情。 当然,这个决定仍然是针对单个案件的 – 没有人“这是正确的”答案。 对于我自己的情况,我可能会用ctypes,但我也期待在其他项目中尝试Cython。

由于没有一个真正的答案,接受一个有点武断。 我select了FogleBird的答案,因为它提供了对ctypes的一些很好的见解,目前它也是最高票数的答案。 不过,我build议阅读所有的答案,以获得良好的概述。

再次感谢。

ctypes是你最好的select,因为你还在编写Python,所以很高兴与你合作!

我最近包装了一个FTDI驱动程序,用ctypes与USB芯片进行通信,这非常棒。 我已经完成了这一切,并在不到一个工作日的工作。 (我只实现了我们需要的function,大概有15个function)。

我们之前使用第三方模块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 

有人在各种选项上做了一些基准testing 。

我可能会更犹豫,如果我不得不包装一个C + +库的许多类/模板/等。 但是ctypes可以很好的与结构体结合,甚至可以callback Python。

警告:一个Cython核心开发者的意见。

我几乎总是推荐Cython的ctypes。 原因是升级path更平滑。 如果你使用ctypes,很多事情一开始就很简单,用纯Python编写FFI代码是很酷的,没有编译,编译依赖等等。 但是,从某种意义上说,你几乎可以肯定地发现,你必须很多地调用你的C库,无论是在一个循环中,还是在一系列相互依存的调用中,你都想加快速度。 这就是你会注意到你不能用ctypes做到这一点的地步。 或者,当你需要callback函数,并且你发现你的Pythoncallback代码成为了一个瓶颈的时候,你想要加速和/或将它移到C中。 再一次,你不能用ctypes做到这一点。 所以你必须在这个时候切换语言,并开始重写你的代码的一部分,有可能将你的Python / ctypes代码逆向工程化为纯C语言,从而破坏了用普通Python写代码的好处。

使用Cython,OTOH,您可以完全自由地使包装和调用代码尽可能薄或厚。 您可以从常规Python代码简单地调用您的C代码,并且Cython将它们转换为本地C调用,而不会有额外的调用开销,并且对Python参数的转换开销极低。 当你注意到你需要更多的性能的时候,你需要花费太多的代价来调用你的C库,你可以用静态types来注释周围的Python代码,并让Cython将它直接优化为C。 或者,您可以开始在Cython中重写C代码的一部分,以避免调用,并专门化和收紧您的循环algorithm。 如果你需要一个快速的callback,只需要用适当的签名编写一个函数,并直接把它传递给Ccallbackregistry。 再次,没有开销,它给你明确的C调用性能。 在Cython中,如果你真的无法获得足够快的代码,你仍然可以考虑用C(或者C ++或者Fortran)重写真正关键的部分,并且从你的Cython代码中自然地调用它。 但是,这真的成为最后的手段,而不是唯一的select。

所以,ctypes很高兴做简单的事情,并迅速得到一些东西运行。 然而,一旦事情开始增长,你很可能会发现你最好从一开始就使用Cython。

Cython本身是一个非常酷的工具,非常值得学习,而且与Python的语法非常接近。 如果你使用Numpy进行任何科学计算,那么Cython就是要走的路,因为它与Numpy集成在一起进行快速matrix运算。

Cython是Python语言的超集。 你可以抛出任何有效的Python文件,它会吐出一个有效的C程序。 在这种情况下,Cython只会将Python调用映射到底层的CPython API。 这可能导致加速50%,因为你的代码不再被解释。

为了得到一些优化,你必须开始告诉Cython关于你的代码的其他事实,比如types声明。 如果你足够的话,它可以把代码烧到纯C中。也就是说,Python中的for循环变成了C中的for循环。在这里你将看到巨大的速度增益。 你也可以在这里链接到外部的C程序。

使用Cython代码也非常容易。 我认为这本手册听起来很难。 你从字面上只是做:

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

然后你可以在你的Python代码中import mymodule ,并完全忘记它编译为C

无论如何,因为Cython非常容易安装并开始使用,所以我build议您尝试一下,看它是否适合您的需求。 如果事实certificate你不是在寻找的工具,这不会是一种浪费。

为了从Python应用程序中调用C库, cffi也是ctypes的新select。 它为FFI带来了新的面貌:

  • 它以一种迷人,干净的方式处理问题(与ctypes相反)
  • 它不需要编写非Python代码(如SWIG,Cython ,…)

我会再扔一个: SWIG

这很容易学习,做很多事情,并支持更多的语言,所以花时间学习它可以是非常有用的。

如果你使用SWIG,你正在创build一个新的Python扩展模块,但SWIG为你做了大部分工作。

就个人而言,我会用C编写一个扩展模块。不要被Python C扩展吓倒 – 他们一点也不难写。 该文件非常清晰,有帮助。 当我第一次在Python中编写C扩展时,我想我花了大约一个小时才弄清楚如何编写一个 – 没有多less时间。

当你已经有一个编译的库blob来处理(如操作系统库)时, ctypes是非常棒的。 然而,调用的开销是非常严重的,所以如果你要进入图书馆的很多调用,而且你将要编写C代码(或者至less编译它),我会说去cython 。 这没什么更多的工作,而且使用生成的pyd文件会更快,更麻烦。

我个人倾向于使用cython来快速加速python代码(循环和整数比较是cython特别发光的两个区域),当涉及到其他库的代码/包装时,我将转向Boost.Python 。 Boost.Python可以设置,但一旦你得到它的工作,它使得包装C / C ++代码直接。

cython也很好包装numpy (我从SciPy 2009会议中学到的),但是我没有使用numpy,所以我不能评论这个。

如果你已经有一个定义了API的库,我认为ctypes是最好的select,因为你只需要做一些初始化,然后或多或less地按照习惯的方式调用库。

我认为当你需要新的代码时,Cython或者在C中创build一个扩展模块(这不是很困难)会更有用,比如调用这个库并执行一些复杂的,耗时的任务,然后将结果传递给Python。

对于简单的程序,另一种方法是直接做一个不同的过程(外部编译),将结果输出到标准输出,并用subprocess模块调用它。 有时候这是最简单的方法。

例如,如果你制作一个或多或less的控制台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 

用一个小string格式化,你可以以任何你想要的结果。 您还可以捕获标准的错误输出,所以它非常灵活。

有一个问题,使我使用ctypes而不是cython,而其他答案中没有提到。

使用ctypes的结果不依赖于你正在使用的编译器。 你可以使用或多或less的语言来编写一个库,这些语言可以被编译为本地共享库。 没关系,哪个系统,哪个语言和哪个编译器。 然而,Cython受基础设施的限制。 例如,如果你想在Windows上使用intel编译器,使cython工作起来要麻烦得多:你应该把编译器“解释”到cython,用这个精确的编译器重新编译一些东西,等等,这极大地限制了可移植性。

如果您的目标是Windows并select包装一些专有的C ++库,那么您可能很快就会发现不同版本的msvcrt***.dll (Visual C ++ Runtime)略有不兼容。

这意味着你可能无法使用Cython因为wrapper.pyd链接到msvcr90.dll (Python 2.7)msvcr100.dll (Python 3.x) 。 如果你正在包装的库链接到不同版本的运行时,那么你运气不好。

然后为了使事情工作,你需要为C ++库创buildC包装器,将包装器DLL与msvcrt***.dll版本链接到C ++库。 然后使用ctypes在运行时dynamic加载您的手动压缩包DLL。

所以有很多小的细节,在下面的文章中有详细的描述:

“美丽的本土图书馆(Python) ”: http : //lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/

对于使用GLib的库,也有一种可能性使用GObject Introspection 。