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

时间:2009-12-21 20:05:52

标签: python c ctypes cython

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

  1. 在C中创建一个实际的扩展模块。可能是矫枉过正,我也想避免学习扩展写作的开销。
  2. 使用Cython将C库中的相关部分公开给Python。
  3. 使用ctypes与外部库进行通信,在Python中执行所有操作。
  4. 我不确定2)或3)是否是更好的选择。 3)的优点是ctypes是标准库的一部分,结果代码将是纯Python - 尽管我不确定这个优势究竟有多大。

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


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

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

    再次感谢。

12 个答案:

答案 0 :(得分:139)

警告:未来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 :(得分:106)

ctypes是您快速完成任务的最佳选择,并且很高兴与您一起工作,因为您还在编写Python!

我最近包装了一个FTDI驱动程序,用于使用ctypes与USB芯片进行通信,这很棒。我在不到一个工作日内完成了所有工作。 (我只实现了我们需要的功能,大约15个功能)。

我们以前使用第三方模块PyUSB用于同一目的。 PyUSB是一个实际的C / Python扩展模块。但是当阻塞读/写时,PyUSB没有发布GIL,这给我们带来了问题。所以我使用ctypes编写了自己的模块,它在调用本机函数时会释放GIL。

有一点需要注意的是,ctypes不会知道你正在使用的库中的#define常量和东西,只知道函数,所以你必须在你自己的代码中重新定义这些常量。 / p>

这是一个代码如何最终查找的例子(大量剪辑,只是试图向您展示它的要点):

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

如果我不得不用一个包含很多类/模板/等的C ++库来包装,我可能会更犹豫不决。但ctypes适用于结构,甚至可以callback进入Python。

答案 2 :(得分:95)

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很容易设置并开始使用,我建议尝试一下,看看它是否适合您的需求。如果事实证明不是你正在寻找的工具,那将不会是浪费。

答案 3 :(得分:40)

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

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

答案 4 :(得分:20)

我会再扔一个:SWIG

它易于学习,做了很多事情,支持更多语言,因此学习它的时间非常有用。

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

答案 5 :(得分:18)

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

答案 6 :(得分:10)

当你已经有一个编译的库blob来处理(例如OS库)时,

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

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

cython也非常擅长包裹Boost.Python(我从numpy学到的),但我没有使用过numpy,所以我不能对此发表评论。

答案 7 :(得分:9)

如果你已经有一个带有已定义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

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

答案 8 :(得分:6)

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

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

答案 9 :(得分:3)

如果您的目标是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/

答案 10 :(得分:2)

还有一种方法可以将GObject Introspection用于使用GLib的库。

答案 11 :(得分:2)

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

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

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

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

这里详细介绍了我需要花多少时间来实现这两个功能。顺便说一下,我很少进行C / C ++编程:

  • Ctypes:

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

这时,我决定寻找替代方法,并决定研究UnicodeError

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

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

的感觉。