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

bal*_*pha 272 c python ctypes cython

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

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

我不确定2)或3)是否是更好的选择.3)的优点是它ctypes是标准库的一部分,结果代码将是纯Python - 尽管我不确定这个优势实际上有多大.

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


编辑:感谢您的所有答案,他们为希望做类似事情的人提供了一个很好的资源.当然,这个决定仍然是针对单个案例做出的 - 没有人"这是正确的事情"的答案.对于我自己的情况,我可能会选择ctypes,但我也期待在其他项目中尝试Cython.

由于没有一个真正的答案,接受一个有点武断; 我选择了FogleBird的答案,因为它提供了对ctypes的一些很好的洞察力,它目前也是最高投票的答案.但是,我建议阅读所有答案以获得良好的概述.

再次感谢.

小智 148

警告:未来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为你直接优化它.或者,您可以在Cython中开始重写C代码的一部分,以避免调用并在算法上专门化和收紧您的循环.如果你需要快速回调,只需编写一个带有相应签名的函数,然后直接将它传递给C回调注册表.同样,没有开销,它给你简单的C调用性能.在Cython中你真的无法快速获得代码的情况要小得多,你仍然可以考虑用C(或C++或Fortran)重写它的真正关键部分,并自然地和本地地从你的Cython代码中调用它.但是,这真的成为最后的选择,而不是唯一的选择.

因此,ctypes很擅长做简单的事情并快速运行.然而,一旦事情开始增长,你很可能会发现你从一开始就注意到你最好使用Cython.

  • +1这些都是好点,非常感谢!虽然我想知道是否只将部分瓶颈移到Cython上真的是一个开销.但我同意,如果你期望任何性能问题,你可以从一开始就使用Cython. (4认同)

Fog*_*ird 111

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
Run Code Online (Sandbox Code Playgroud)

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

如果我必须用一些包含很多类/模板/等的C++库来包装,我可能会更犹豫不决.但ctypes适用于结构,甚至可以回调到Python.

  • 加入对ctypes的赞美,但注意一个(无证)问题:ctypes不支持分叉.如果你使用ctypes从一个进程派生,并且父进程和子进程都继续使用ctypes,你将偶然发现一个与使用共享内存的ctypes有关的讨厌错误. (5认同)

car*_*arl 98

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
Run Code Online (Sandbox Code Playgroud)

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

无论如何,因为Cython很容易设置并开始使用,我建议尝试一下,看看它是否适合您的需求.如果事实证明这不是您正在寻找的工具,那将不会是浪费.

  • "你可以抛出任何有效的Python文件,它会吐出一个有效的C程序." < - 不完全是,有一些限制:http://docs.cython.org/src/userguide/limitations.html对于大多数用例来说可能不是问题,但只是想完成. (17认同)
  • 每个版本的问题都越来越少,以至于该页面现在说"大多数问题已在0.15中解决". (7认同)
  • 要添加,有一种更简单的方法来导入cython代码:将你的cython代码编写为`mymod.pyx`模块,然后执行`import pyximport; pyximport.install(); 导入mymod`并在后台进行编译. (3认同)
  • @kaushik更简单的是http://pypi.python.org/pypi/runcython.只需使用`runcython mymodule.pyx`即可.与pyximport不同,您可以将它用于更苛刻的链接任务.唯一需要注意的是,我是那个为它编写20行bash并且可能有偏见的人. (3认同)

Rob*_*mba 40

为了从Python应用程序调用C库,还有cffi,它是ctypes的新替代品.它为FFI带来了全新的外观:

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


Chr*_*uin 21

我会再扔一个:SWIG

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

如果您使用SWIG,那么您正在创建一个新的python扩展模块,但是SWIG会为您完成大部分繁重的工作.


mip*_*adi 18

就个人而言,我在C中编写了一个扩展模块.不要被Python C扩展所吓倒 - 他们写起来并不难.文档非常清晰且有用.当我第一次在Python中编写C扩展时,我认为花了大约一个小时来弄清楚如何编写一个 - 没有多少时间.

  • 你包装C库还是编写新的C代码? (2认同)
  • 同意.Python的C-API并不像它看起来那么可怕(假设你知道C).但是,与python及其库,资源和开发人员库不同,在C语言中编写扩展时,基本上就是你自己.可能是它唯一的缺点(除了通常用C语写的那些). (2认同)
  • @mipadi github 链接似乎已失效,并且在 archive.org 上似乎不可用,您有备份吗? (2认同)

Rya*_*rom 10

当你已经有一个编译的库blob来处理时(例如OS库),ctypes很棒.然而,调用开销是严重的,所以如果你要对库进行大量调用,并且你将要编写C代码(或者至少编译它),我会说要去cython.这不是更多的工作,使用生成的pyd文件会更快,更pythonic.

我个人倾向于使用cython来快速加速python代码(循环和整数比较是cython特别闪耀的两个领域),当有更多涉及其他库的代码/包装时,我将转向Boost.Python.Boost.Python可能很难设置,但是一旦你有了它,它就会让C/C++代码变得简单.

cython也非常适合包装numpy(我从SciPy 2009程序中学到的),但是我还没有使用numpy,所以我不能对此发表评论.


Kaa*_* E. 10

我知道这是一个老问题,但是当你搜索类似的东西时,谷歌上就会出现这个问题ctypes vs cython,并且这里的大多数答案都是由那些已经精通的人写的cython,或者c可能无法反映你需要投入学习这些内容的实际时间实施您的解决方案。我在这两方面都是完全的初学者。我以前从来没有接触过cython,经验也很少c/c++

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

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

这是我的判断。使用cython。它与 python 的集成更加流畅,并且总体上更易于使用。当出现问题时,ctypes只会抛出段错误,至少cython会在可能的情况下向您提供带有堆栈跟踪的编译警告,并且您可以使用cython.

这里详细说明了我需要在两者上投入多少时间来实现相同的功能。顺便说一句,我很少进行 C/C++ 编程:

  • C类型:

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

此时,我决定寻找替代方案并决定研究cython

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

根据记录,我当然没有衡量我的投资的确切时间。很可能我对时间的感知有点不够专心,因为我在处理 ctypes 时需要太多的脑力劳动。cython但它应该传达处理和处理的感觉ctypes


Khe*_*ben 9

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

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

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

例如,如果您创建一个或多或少以这种方式工作的控制台C程序

$miCcode 10
Result: 12345678
Run Code Online (Sandbox Code Playgroud)

你可以用Python调用它

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

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


Mis*_*sha 6

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

使用ctypes,结果不依赖于您正在使用的编译器.您可以使用或多或少的任何语言编写库,这些语言可以编译为本机共享库.它没关系,哪个系统,哪个语言和哪个编译器.但是,Cython受到基础设施的限制.例如,如果你想在windows上使用intel编译器,那么让cython工作会更加棘手:你应该将编译器"解释"为cython,用这个精确的编译器重新编译一些东西等.这极大地限制了可移植性.


ilj*_*jau 5

如果您面向 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/

  • +1提到了Windows上可能存在的问题(我目前也遇到了......)。@IanH 一般来说,它与 Windows 无关,但如果你坚持使用与你的 python 发行版不匹配的给定第三方库,那就会很混乱。 (2认同)