我想从Python应用程序调用C库。我不想包装整个API,只包装与我的案例相关的函数和数据类型。在我看来,我有三个选择:
用c语言创建一个实际的扩展模块,这可能有点过分,而且我还想避免学习编写扩展的开销。
使用Cython将相关部分从C库公开到Python。
在Python中完成所有的事情,使用ctypes与外部库通信。
我不知道2)还是3)哪个更好。3)的优点是ctypes是标准库的一部分,生成的代码将是纯Python——尽管我不确定这个优点到底有多大。
这两种选择有更多的优点/缺点吗?你推荐哪种方法?
编辑:感谢你的回答,它们为任何想做类似事情的人提供了很好的资源。当然,这个决定仍然是针对单一情况做出的——没有一个“这是正确的事情”之类的答案。对于我自己的情况,我可能会使用ctypes,但我也期待在其他一些项目中尝试Cython。
由于没有唯一的正确答案,接受一个答案就有些武断了;我选择了FogleBird的答案,因为它提供了一些关于ctypes的很好的见解,而且它也是目前投票最多的答案。然而,我建议阅读所有的答案,以获得一个良好的概述。
再次感谢。
如果您的目标是Windows并选择包装一些专有的c++库,那么您可能很快就会发现msvcrt***.dll (Visual c++ Runtime)的不同版本略有不兼容。
这意味着您可能无法使用Cython,因为产生了包装器。pyd链接到msvcr90.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/
有一个问题让我使用ctypes而不是cython,这在其他答案中没有提到。
Using ctypes the result does not depend on compiler you are using at all. You may write a library using more or less any language which may be compiled to native shared library. It does not matter much, which system, which language and which compiler. Cython, however, is limited by the infrastructure. E.g, if you want to use intel compiler on windows, it is much more tricky to make cython work: you should "explain" compiler to cython, recompile something with this exact compiler, etc. Which significantly limits portability.
如果你已经有了一个定义了API的库,我认为ctypes是最好的选择,因为你只需要做一点初始化,然后或多或少地以你习惯的方式调用库。
我认为当你需要新代码时,Cython或用C创建一个扩展模块(这并不难)更有用,例如调用那个库并执行一些复杂、耗时的任务,然后将结果传递给Python。
对于简单的程序,另一种方法是直接执行不同的进程(外部编译),将结果输出到标准输出,并使用subprocess模块调用它。有时这是最简单的方法。
例如,如果你制作一个控制台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
通过一些字符串格式化,您可以以任何您想要的方式获取结果。您还可以捕获标准错误输出,因此非常灵活。
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。