我想从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都很容易安装和开始使用,所以我建议尝试一下它是否适合您的需求。如果它不是你想要的工具,那也不是浪费。
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。