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

2024-03-28 10:49:10 发布

您现在位置:Python中文网/ 问答频道 /正文

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

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

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

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


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

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

再次感谢。


Tags: 模块函数答案代码api应用程序标准ctypes
3条回答

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是如此容易设置和开始使用,我建议尝试看看它是否适合您的需要。如果它不是你要找的工具,那就不是浪费。

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++库,我可能会更加犹豫,但是cType与结构非常好,甚至可以将{a4}导入到Python中。

警告: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。

相关问题 更多 >