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


284

我想从Python应用程序调用C库。我不想包装整个API,只包装与我的情况相关的函数和数据类型。如我所见,我有三个选择:

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

我不确定2)还是3)是更好的选择。3)的优点是它ctypes是标准库的一部分,并且生成的代码将是纯Python –尽管我不确定该优点实际上有多大。

两种选择都有其他优点/缺点吗?您推荐哪种方法?


编辑:感谢您的所有答复,它们为希望做类似事情的任何人提供了很好的资源。当然,仍需针对单个案例做出决定-没有人会回答“这是对的”。就我自己而言,我可能会使用ctypes,但我也期待在其他项目中试用Cython。

由于没有一个单一的真实答案,因此接受一个答案有点武断。我选择了FogleBird的答案,因为它提供了对ctypes的一些很好的了解,并且它也是当前投票最高的答案。但是,我建议您阅读所有答案以获得一个很好的概述。

再次感谢。


3
在某种程度上,所涉及的特定应用程序(库的工作)可能会影响方法的选择。我们已经非常成功地使用ctypes与供应商提供的DLL进行了各种困难的交流(例如示波器),但是由于与Cython或SWIG相比,额外的开销,因此我不一定会选择ctypes与数字处理库进行通信。
彼得·汉森

1
现在您有了想要的东西。四个不同的答案(有人也发现了SWIG)。是不是意味着现在你有4个选项,而不是3
卢卡Rahne

@ralu这也是我的意思:-)但是,严重的是,我没想到(或想要)一个赞成/反对表或一个回答说“这就是你需要做的”。关于决策的任何问题最好由每个可能选择的“粉丝”给出其原因来回答。然后,社区投票和我自己的工作一样发挥作用(查看论点,将其应用于我的案例,阅读提供的信息等)。长话短说:这里有一些很好的答案。
balpha

那么您打算采用哪种方法?:)
FogleBird

1
据我所知(如果我错了,请纠正我),Cython是Pyrex的一个分支,正在投入更多的开发工作,这使Pyrex几乎过时了。
balpha

Answers:


115

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中。


5
加入对ctypes的赞誉,但要注意一个(未记录的)问题:ctypes不支持派生。如果您从使用ctypes的进程派生,并且父进程和子进程都继续使用ctypes,您将偶然发现一个讨厌的bug,该bug与使用共享内存的ctypes有关。
Oren Shemesh 2012年

1
@OrenShemesh您可以向我指出有关此问题的进一步阅读吗?我认为我目前正在从事的项目可能是安全的,因为我相信只有父流程使用ctypes(for pyinotify),但是我想更彻底地理解问题。
Zigg 2012年

这段话对我有很大帮助,One thing to note is that ctypes won't know about #define constants and stuff in the library you're using, only the functions, so you'll have to redefine those constants in your own code.所以,我必须定义在winioctl.h... 中存在的常量
swdev

性能如何?ctypes比瓶颈扩展慢得多,因为瓶颈是从Python到C的接口
TomSawyer,

154

警告:Cython核心开发人员的意见。

我几乎总是建议Cython胜过ctypes。原因是它具有更平滑的升级路径。如果使用ctypes,一开始很多事情都会很简单,用纯Python编写FFI代码当然很酷,而无需编译,构建依赖关系以及所有这些。但是,在某个时候,您几乎可以肯定会发现,您必须循环或以较长的一系列相互依赖的调用方式大量调用C库,并且您希望加快速度。在这一点上,您会注意到无法使用ctypes做到这一点。或者,当您需要回调函数并且发现Python回调代码成为瓶颈时,您也想加快它的速度和/或也将它移入C。同样,您不能使用ctypes做到这一点。

使用OTOH的Cython,您可以完全自由地使包装和调用代码变薄或变厚。您可以从常规Python代码对C代码的简单调用开始,然后Cython会将它们转换为本地C调用,而没有任何额外的调用开销,并且Python参数的转换开销非常低。当您发现需要对C库进行太多昂贵的调用时甚至需要更高的性能时,可以开始使用静态类型注释周围的Python代码,并让Cython为您直接对其进行优化。或者,您可以开始在Cython中重写部分C代码,以避免调用并在算法上专门化和加强循环。如果您需要快速回调,只需编写具有适当签名的函数,然后将其直接传递到C回调注册表即可。再次,没有开销,并且它为您提供了普通的C调用性能。而且在不太可能发生的情况下,您实际上无法在Cython中获得足够快的代码,您仍然可以考虑使用C(或C ++或Fortran)重写其真正关键的部分,并自然地从本地从Cython代码中调用它。但是,这实际上成了最后的选择,而不是唯一的选择。

因此,ctypes可以很好地完成简单的事情并快速使某些事情运行。但是,一旦事情开始发展,您很可能会发现您最好从一开始就使用Cython。


4
+1这些是好点,非常感谢!尽管我想知道是否仅将瓶颈部分移动到Cython确实是这么大的开销。但是我同意,如果您希望遇到任何性能问题,那么不妨从一开始就使用Cython。
balpha

这对于拥有C和Python经验的程序员仍然有效吗?在那种情况下,可能会说Python / ctypes是更好的选择,因为C循环(SIMD)的向量化有时更直接。但是,除此之外,我无法想到任何Cython的缺点。
Alex van Houten'3

感谢您的回答!关于Cython我遇到的一件事是正确地构建了程序(但这也与我以前从未编写过Python模块有关)–我应该在此之前对其进行编译,还是在sdist和类似问题中包含Cython源文件。我写了一篇关于它的博客文章,以防万一有人有类似的问题/怀疑:martinsosic.com/development/2016/02/08/…–
Martinsos

感谢您的回答!我使用Cython的一个缺点是没有完全实现运算符重载(例如__radd__)。当您计划类与内置类型(例如intfloat)进行交互时,这尤其令人讨厌。而且,cython中的魔术方法一般来说还有些小问题。
Monolith

100

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文件即可。
卡尔,

18
“您可以向其抛出任何有效的Python文件,它将吐出有效的C程序。” <-不完全是,但有一些限制:docs.cython.org/src/userguide/limitations.html 对于大多数用例来说,可能不是问题,但只是想完整一点即可。
兰迪·赛林

7
每个版本的问题越来越少,以至于该页面现在说“大多数问题已在0.15中解决”。
亨利·戈默索尔

3
另外,还有一种导入cython代码的简便方法:将cython代码作为mymod.pyx模块编写,然后执行import pyximport; pyximport.install(); import mymod,编译就在后台进行。
Kaushik Ghose

3
@kaushik甚至更简单的是pypi.python.org/pypi/runcython。只需使用runcython mymodule.pyx。与pyximport不同,您可以将其用于更苛刻的链接任务。唯一需要注意的是,我是为此写过20行bash的人,并且可能会有偏见。
RussellStewart 2015年

42

为了从Python应用程序调用C库,还有cffi,它是ctypes的新选择。它为FFI带来了全新外观:

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

绝对是OP想要的包装方式。cython听起来很适合自己编写热循环,但是对于接口而言,cffi只是ctypes的直接升级。
飞羊

21

我再扔一个:SWIG

它易于学习,可以正确完成许多事情,并且支持更多语言,因此花时间学习它会非常有用。

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


18

就个人而言,我会用C语言编写一个扩展模块。不要被Python C扩展吓到了-它们一点也不难编写。该文档非常清楚并且很有帮助。当我第一次用Python编写C扩展时,我认为花了大约一个小时才弄清楚如何编写一个-根本没有多少时间。


包装一个C库。:实际上,你可以在这里找到代码github.com/mdippery/lehmer
mipadi

1
@forivall:代码实际上并没有那么有用,并且那里有更好的随机数生成器。我的计算机上只有一个备份。
mipadi 2012年

2
同意 Python的C-API并不像看起来那样可怕(假设您知道C)。但是,与python及其库,资源和开发人员的库不同,使用C编写扩展时,您基本上是自己一个人。可能是它唯一的缺点(除了通常用C编写的缺点)。
Noob Saibot 2014年

1
@mipadi:是的,但是它们在Python 2.x和3.x之间有所不同,因此使用Cython编写扩展名,让Cython找出所有细节然后为Python 2.x Windows编译生成的C代码更加方便。3.x根据需要。
0xC0000022L

2
@mipadi似乎github链接已死,并且archive.org上似乎不可用,您有备份吗?
jrh

11

当您已经有编译的库blob要处理时(例如OS库),ctypes很棒。但是,调用开销很大,因此,如果您要在库中进行大量调用,并且无论如何都要编写C代码(或至少要编译它),我会说赛顿。它不需要太多工作,并且使用生成的pyd文件会更快,更pythonic。

我个人倾向于使用cython来加快python代码的速度(循环和整数比较是cython尤为突出的两个领域),并且当涉及到更多涉及代码/其他库的包装时,我将转向Boost.Python。。Boost.Python的设置可能很繁琐,但是一旦它开始工作,它将使包装C / C ++代码变得简单。

cython也非常擅长包装numpy(这是我从SciPy 2009程序中了解到的),但是我没有使用numpy,所以我无法对此发表评论。


11

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

我认为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

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


尽管这个答案没有什么不妥,但是如果要开放代码供其他人访问,人们应该格外小心,因为shell=True当用户确实获得外壳时,调用子进程很容易导致某种利用。当开发人员是唯一的用户时,这很好,但是在世界范围内,有很多烦人的花招正等着这样的事情。
2015年

7

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

使用ctypes的结果根本不取决于您使用的编译器。您可以或多或少地使用可以编译为本机共享库的任何语言来编写库。哪种系统,哪种语言和哪种编译器都没关系。但是,Cython受基础架构的限制。例如,如果您想在Windows上使用Intel编译器,则使cython正常工作要困难得多:您应将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***.dllC ++库相同的版本。然后用于ctypes在运行时动态加载手动包装的dll。

因此,有很多小细节,下面的文章中将对其进行详细描述:

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


该文章与您提出的与Microsoft编译器兼容性有关的问题没有任何关系。让Cython扩展程序在Windows上运行确实不是很难。我已经能够使用MinGW进行几乎所有操作。良好的Python发行版对您有所帮助。
IanH 2014年

2
+1表示Windows上可能存在的问题(我目前也有...)。@IanH通常来说,它与Windows无关,但是如果您坚持使用与python发行版不匹配的给定第三方lib,那就太麻烦了。
sebastian 2014年


2

我知道这是一个老问题,但是当您搜索诸如之类的东西时,这件事就会出现在Google上ctypes vs cython,并且这里的大多数答案都是由精通这些知识的人编写的,cython或者c可能无法反映您学习这些所需要的实际时间实施您的解决方案。我都是这方面的初学者。我以前从未接触过cython,并且经验很少c/c++

在过去的两天里,我一直在寻找一种方法来将代码中性能很重要的部分委托给比python更底层的东西。我在ctypes和中都实现了我的代码Cython,该代码基本上由两个简单的函数组成。

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

这是我的判决。使用cython。它与python的集成更加流畅,并且通常更易于使用。当出现问题时,ctypes只会引发段错误,至少cython会在可能的情况下为您提供带有堆栈跟踪的编译警告,并且您可以使用轻松返回有效的python对象cython

这是有关我需要花多少时间来实现这两个功能的详细说明。顺便说一下,我很少进行C / C ++编程:

  • C类型:

    • 关于研究如何将unicode字符串列表转换为ac兼容类型的大约2小时。
    • 关于如何从ac函数正确返回字符串的大约一个小时。在编写函数之后,实际上我在这里为SO提供了自己的解决方案。
    • 用c编写代码大约半小时,然后将其编译到动态库中。
    • 10分钟在python中编写测试代码以检查c代码是否有效。
    • 做一些测试并重新排列c代码大约一个小时。
    • 然后,我将c代码插入到实际的代码库中,发现该模块ctypes不能很好地与multiprocessing模块配合使用,因为默认情况下无法选择其处理程序。
    • 大约20分钟后,我重新排列了代码以不使用multiprocessing模块,然后重试。
    • 然后,c尽管我的代码中的第二个功能通过了我的测试代码,但仍在我的代码库中产生了段错误。好吧,这可能是我不能很好地检查边缘情况的错,我一直在寻找一种快速的解决方案。
    • 在大约40分钟的时间里,我试图确定这些段错误的可能原因。
    • 我将函数分为两个库,然后重试。我的第二个功能仍然存在段错误。
    • 我决定放开第二个函数,只使用c代码的第一个函数,并且在使用它的python循环的第二或第三次迭代中,UnicodeError尽管我进行了一切编码和解码,但我仍未在某个位置解码字节明确地

在这一点上,我决定寻找一种替代方法,并决定研究cython

  • 赛顿
    • 阅读cython hello world的 10分钟。
    • 15分钟内检查SO如何使用cython setuptools代替distutils
    • 关于cython类型和python类型的阅读,需要 10分钟。我了解到我可以使用大多数内置的python类型进行静态输入。
    • 用cython类型重新注释我的python代码的15分钟。
    • 修改setup.py我的代码库中使用编译模块的10分钟。
    • 将模块直接插入multiprocessing代码库的版本。有用。

根据记录,我当然没有衡量投资的确切时机。很可能是由于我在处理ctypes时需要付出精神上的努力,所以我对时间的感觉有些不专心。但是,应该传达处理的手感cythonctypes

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.