301

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

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

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

任何一种选择都有更多的优点/缺点吗?您推荐哪种方法?


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

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

再次感谢。

4

12 回答 12

171

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

我几乎总是推荐 Cython 而不是 ctypes。原因是它的升级路径更加顺畅。如果你使用 ctypes,很多事情一开始会很简单,用纯 Python 编写你的 FFI 代码当然很酷,无需编译、构建依赖和所有这些。但是,在某些时候,您几乎肯定会发现您必须多次调用您的 C 库,无论是在循环中还是在更长的一系列相互依赖的调用中,并且您希望加快速度。这就是您会注意到使用 ctypes 无法做到这一点的地方。或者,当您需要回调函数并且发现 Python 回调代码成为瓶颈时,您希望加快它的速度和/或将其移至 C 语言中。同样,你不能用 ctypes 做到这一点。

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

因此,ctypes 很适合做简单的事情并快速运行。但是,一旦事情开始发展,您很可能会注意到您最好从一开始就使用 Cython。

于 2011-04-16T13:41:45.280 回答
125

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

于 2009-12-21T20:37:14.927 回答
105

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

于 2009-12-21T20:32:22.000 回答
43

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

  • 它以一种引人入胜、干净的方式处理问题(与ctypes 不同
  • 它不需要编写非 Python 代码(如SWIG、Cython等)
于 2013-02-18T17:33:30.420 回答
22

我会再扔一个:SWIG

它很容易学习,可以做很多正确的事情,并且支持更多的语言,所以花在学习上的时间会非常有用。

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

于 2009-12-21T20:09:31.287 回答
19

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

于 2009-12-21T20:36:42.803 回答
12

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

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

于 2009-12-21T20:37:56.043 回答
12

当您已经有一个已编译的库 blob 需要处理(例如 OS 库)时,ctypes非常棒。然而,调用开销很严重,所以如果你要对库进行大量调用,并且无论如何你都要编写 C 代码(或至少编译它),我会说去赛通。这不是更多的工作,使用生成的 pyd 文件会更快,更 Pythonic。

我个人倾向于使用 cython 来快速加速 python 代码(循环和整数比较是 cython 特别突出的两个领域),当涉及更多涉及的其他库的代码/包装时,我将转向Boost.Python。Boost.Python 的设置可能很繁琐,但一旦你开始使用它,它就可以直接包装 C/C++ 代码。

cython 也很擅长包装numpy(我从SciPy 2009 程序中学到的),但我没有使用过 numpy,所以我不能对此发表评论。

于 2009-12-27T15:13:27.583 回答
9

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

使用 ctypes 结果完全不依赖于您使用的编译器。您可以使用或多或少的任何可以编译为本机共享库的语言来编写库。哪个系统,哪种语言和哪种编译器并不重要。然而,Cython 受到基础设施的限制。例如,如果你想在 Windows 上使用 intel 编译器,那么让 cython 工作起来要棘手得多:你应该向 cython “解释”编译器,用这个精确的编译器重新编译一些东西,等等。这极大地限制了可移植性。

于 2013-06-12T14:15:18.040 回答
6

我知道这是一个老问题,但是当你搜索类似的东西时,这个东西会出现在谷歌上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++ 编程:

  • 类型:

    • 大约 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

于 2020-01-04T03:47:56.110 回答
5

如果您以 Windows 为目标并选择封装一些专有的 C++ 库,那么您可能很快就会发现不同版本的msvcrt***.dll(Visual C++ Runtime)略有不兼容。

这意味着您可能无法使用,Cython因为结果wrapper.pydmsvcr90.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/

于 2014-02-23T11:31:10.273 回答
3

对于使用GLib的库,还有一种可能性是使用GObject Introspection

于 2011-09-01T05:19:39.427 回答