在Python中包装C库:C, Cython或ctypes?

我想从Python应用程序调用C库。我不想包装整个API,只包装与我的案例相关的函数和数据类型。在我看来,我有三个选择:

  1. 用c语言创建一个实际的扩展模块,这可能有点过分,而且我还想避免学习编写扩展的开销。
  2. 使用Cython将相关部分从C库公开到Python。
  3. 在Python中完成整个过程,使用ctypes与外部库通信。

我不知道2)还是3)哪个更好。3)的优点是ctypes是标准库的一部分,生成的代码将是纯Python –虽然我不确定这个优势到底有多大。

这两种选择有更多的优点/缺点吗?你推荐哪种方法?


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

由于没有唯一的正确答案,接受一个答案就有些武断了;我选择了FogleBird的答案,因为它提供了一些关于ctypes的很好的见解,而且它也是目前投票最多的答案。然而,我建议阅读所有的答案,以获得一个良好的概述。

再次感谢。

86123 次浏览

我再抛出另一个:痛饮

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

如果您使用SWIG,您将创建一个新的python扩展模块,但是SWIG将为您完成大部分繁重的工作。

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

然后你可以在你的Python代码中import mymodule,完全忘记它可以编译成C语言。

在任何情况下,由于Cython都很容易安装和开始使用,所以我建议尝试一下它是否适合您的需求。如果它不是你想要的工具,那也不是浪费。

就我个人而言,我会用C写一个扩展模块。不要被Python C扩展吓倒——它们写起来一点都不难。文档是非常清晰和有用的。当我第一次用Python写一个C扩展时,我想我花了大约一个小时来弄清楚如何写一个——根本没有多少时间。

ctypes是你快速完成它的最佳选择,当你仍然在编写Python时,使用它是一种乐趣!

我最近包装了一个FTDI驱动程序,用于使用ctypes与USB芯片通信,它很棒。我在不到一个工作日的时间里完成了所有的工作。(我只实现了我们需要的函数,大约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

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

我可能会更犹豫,如果我必须包装一个c++库与许多类/模板/等。但是ctypes可以很好地与结构体一起工作,甚至可以将回调转换为Python。

如果你已经有了一个带有已定义API的库,我认为ctypes是最好的选择,因为你只需要做一点初始化,然后或多或少地以你习惯的方式调用库。

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

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

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

通过一些字符串格式化,您可以以任何您想要的方式获取结果。您还可以捕获标准错误输出,因此非常灵活。

当你已经有一个编译好的库blob要处理(比如OS库)时,ctypes非常有用。然而,调用开销是严重的,所以如果你将对库进行大量调用,并且无论如何你都将编写C代码(或至少编译它),我会说去cython。这并不需要做更多的工作,而且使用生成的pyd文件会更快、更python化。

我个人倾向于使用cython来快速加速python代码(循环和整数比较是cython特别擅长的两个领域),当涉及到其他库的一些更复杂的代码/包装时,我会转向提振。Python。提振。Python的设置可能很挑剔,但一旦你让它工作了,它就可以简单地包装C/ c++代码。

cython在包装numpy方面也很出色(这是我从SciPy 2009会议记录中学到的),但我没有使用numpy,所以我不能对此发表评论。

警告:以下是Cython核心开发人员的观点。

我几乎总是推荐Cython而不是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。

对于使用口齿伶俐的的库,也可以使用GObject自省

对于从Python应用程序调用C库,还有< >强cffi < / >强,这是ctypes的新替代方案。它为FFI带来了全新的面貌:

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

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

使用ctypes的结果完全不依赖于你正在使用的编译器。您可以使用或多或少任何可以编译为本地共享库的语言来编写库。使用哪种系统、哪种语言和哪种编译器都不太重要。然而,Cython受到基础设施的限制。例如,如果你想在windows上使用英特尔编译器,那么让cython工作就更加棘手了:你应该向cython“解释”编译器,用这个编译器重新编译一些东西,等等。这大大限制了可移植性。

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

这意味着你可能无法使用Cython,因为生成的wrapper.pydmsvcr90.dll (Python 2.7)msvcr100.dll (Python 3. x)相关联。如果您正在包装的库链接到不同版本的运行时,那么您就不走运了。

然后,为了让事情正常工作,你需要为c++库创建C包装器,将包装器dll链接到与c++库相同的msvcrt***.dll版本。然后使用ctypes在运行时动态加载你的手卷包装器dll。

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

“美丽的本地库(在Python中)”:http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/

我知道这是一个老问题,但当你搜索像ctypes vs cython这样的东西时,这个东西会在谷歌上出现,这里的大多数答案都是由那些已经精通cythonc的人写的,这可能不能反映你需要投入学习这些来实现你的解决方案的实际时间。这两方面我都是初学者。我以前从未接触过cython,并且对c/c++的经验很少。

在过去的两天里,我一直在寻找一种方法,将我代码中性能较重的部分委托给比python更低级的东西。我在ctypesCython中实现了我的代码,它基本上由两个简单的函数组成。

我有一个巨大的字符串列表需要处理。注意liststring。 这两种类型都不完全对应于c中的类型,因为python字符串默认是unicode,而c字符串不是。python中的列表不是c的数组。

以下是我的看法。使用cython。它更流畅地集成到python中,而且一般来说更容易使用。当出现问题时,ctypes只是抛出段错误,至少cython会在可能的情况下给你一个带有堆栈跟踪的编译警告,并且你可以使用cython轻松返回一个有效的python对象。

下面是关于我需要投入多少时间来实现相同的功能的详细说明。顺便说一下,我做了很少的C/ c++编程:

  • < p > Ctypes:

    • 关于研究如何将我的unicode字符串列表转换为c兼容类型的2h。
    • 关于如何从c函数中正确返回字符串,大约花了一个小时。在这里,一旦我写完函数,我实际上就提供了自己的所以解决方案。
    • 用c语言编写代码大约需要半个小时,将其编译成动态库。
    • 10分钟用python写一个测试代码来检查c代码是否工作。
    • 大约一个小时的测试和重新安排c代码。
    • 然后我将c代码插入到实际的代码库中,并看到ctypes不能很好地使用multiprocessing模块,因为默认情况下它的处理程序是不可选的。
    • 大约20分钟,我重新安排了我的代码,不使用multiprocessing模块,并重试。
    • 然后我的c代码中的第二个函数在我的代码库中生成了段错误,尽管它通过了我的测试代码。好吧,这可能是我的错,因为我没有很好地检查边缘情况,我正在寻找一个快速解决方案。
    • 在大约40分钟的时间里,我试图确定这些段错误的可能原因。
    • 我把我的函数分成两个库,然后再试一次。我的第二个函数仍然有段错误。
    • 我决定放弃第二个函数,只使用c代码的第一个函数,在使用它的python循环的第二次或第三次迭代中,我有一个UnicodeError关于不解码某个位置的字节,尽管我显式地编码和解码了所有内容。

在这一点上,我决定寻找一个替代方案,并决定研究cython:

    <李> Cython
    • 10分钟阅读赛昂,你好,世界
    • 15分钟的检查所以如何使用cython与setuptools代替distutils
    • 10分钟关于cython类型和python类型的阅读。我了解到我可以使用大多数内置的python类型进行静态类型。
    • 15分钟用cython类型重新注释我的python代码。
    • 10分钟的修改我的setup.py使用编译模块在我的代码库。
    • 直接将模块插入到multiprocessing版本的代码库中。它的工作原理。

郑重声明,我当然没有衡量我投资的准确时机。这很可能是由于在处理ctypes时需要花费太多精力,所以我对时间的感知有点太专注了。但它应该传达处理cythonctypes的感觉