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

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

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

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


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

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

再次感谢。


当前回答

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

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

其他回答

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代码中导入mymodule,完全忘记它可以编译成C语言。

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

对于使用GLib的库,也可以使用GObject Introspection。

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

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

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

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

cython在包装numpy方面也很出色(这是我从SciPy 2009会议中了解到的),但我没有使用过numpy,因此不能对此进行评论。

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

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