Python通过ctypes调用C程序实例
在做复杂计算时,Python的执行效率是无法同C比的。而且有些算法已经有开源的C库了,我们也没必要用Python重写一份。那Python怎么调用C语言写的程序库呢?答案就在Python提供的ctypes库,它提供同C语言兼容的数据类型,可以很方便地调用C语言动态链接库中的函数。
使用C标准库函数
我们来试试调用C标准库函数:
from ctypes import cdll
libc = cdll.LoadLibrary('libc.so.6') # Load standard C library on Linux
# libc = cdll.LoadLibrary('libc.dylib') # Load standard C library on Mac
# libc = cdll.msvcrt # Load standard C library on Windows
print libc.time(None)
上述代码加载了Linux中的C标准库”libc.so.6”,并调用其中time()
函数,执行后屏幕上会打印出当前时间戳。注,Windows和Mac上的加载方法在注释中。
调用自定义的动态链接库
我们先根据这篇文章写个动态链接库,现在你有了库”libhello.so”,其有一个”hello”函数。让我们在Python中调用它:
from ctypes import cdll
libhello= cdll.LoadLibrary("./libhello.so")
libhello.hello('You')
看到屏幕上”Hello You!“的字样了吧。对!就这么简单,比起Java调用本地代码方便很多吧。注意,本例中的”libhello.so”同Python程序在同一目录下。
效率对比
我们写个阶乘(factorial)函数,来比较Python和C的执行效率吧。先写C代码:
int factorial(int n)
{
if (n < 2)
return 1;
return factorial(n - 1) * n;
}
方便起见,我们把它放在之前写的”hello.c”文件中,这样就可以从”libhello.so”中调用它。别忘了在”hello.h”中声明这个函数。然后实现Python代码:
def factorial(n):
if n < 2:
return 1
return factorial(n - 1) * n
def factorial_c(n):
return libhello.factorial(n)
Python的实现可以说同C的一模一样,我们另外定义一个factorial_c()
函数来封装C的调用。现在,我们来比较下执行效率。这里要引入Python的timeit
包,它可以帮你计算程序的执行时间,省去你很多代码。让我们来算20的阶乘,并计算10万次,看看所消耗的时间:
from timeit import timeit
f_setup = 'from __main__ import factorial'
f_c_setup = 'from __main__ import factorial_c'
print timeit('factorial(20)', setup=f_setup, number=100000)
print timeit('factorial_c(20)', setup=f_c_setup, number=100000)
我在虚拟机上跑的结果结果是:
0.231598138809
0.0475780963898
差不多5倍的差距啊!
参数传址
大家知道C的函数参数是传值的(其实Python也一样),那我想在C中改变参数的值怎么办,那就需要传递引用了。我们在上面的”libhello.so”中加一个快排函数:
void quickSort(int *a, int start, int end)
{
if (start < end)
{
int left = start;
int right = end;
int key = a[left];
while (left < right)
{
while (left < right && a[right] >= key)
right--;
a[left] = a[right];
while (left < right && a[left] <= key)
left++;
a[right] = a[left];
}
a[left] = key;
quickSort(a, start, right-1);
quickSort(a, left + 1, end);
}
}
朋友们马上可以看出,这段函数中数组a中的值是可以被改变的。那Python怎么调用它呢?就是在参数传递时,加上byref()
调用,它是ctypes提供的方法,如果用它调用int型变量a时,作用类似于(int *) &a
。所以我们的Python程序可以这样写:
from ctypes import cdll, c_int, byref
def quick_sort(numbers):
size = len(numbers)
c_numbers = (c_int * size)(*numbers)
libhello.quickSort(byref(c_numbers), 0, size)
return c_numbers
这里还有个知识点,就是C类型。为了同C的变量类型兼容,ctypes库提供了一系列对应的C类型。本例中”c_int”就是对应C中的”int”型。我们将c_int * 10
就等于创建一个长度为10的int型数组。而后面的(*number)
就是把”numbers”的值赋给刚创建的int数组。ctypes库所有提供的C类型可以在这里查到。
上例中,我们必须传入C类型的整型数组才能被C程序接收。现在让我们来使用下这个快排:
from random import shuffle, sample
numbers = sample(range(1000), 99)
shuffle(numbers)
sorted_num = quick_sort(numbers)
for i in sorted_num:
print i
有兴致的朋友们也可以写个Python的快排来比较下效率。
参数及返回类型指定
我们回到C标准库,调用下strchr()
方法,它的作用是在字符串中找出以指定字符开头的子串。
strchr = libc.strchr
print strchr('abcdef', 'd')
你会发现,返回一直是0,而我们期望的应该是”def”。其实,问题是在我们的第二个参数,它应该是一个字符,而Python中它是一个字符串。那怎么让它成为字符类型呢?一个方法是使用strchr('abcdef', ord('d'))
调用strchr()
方法,ord()
函数可以把字符串变成字符类型,但是每次调用都要加上,很麻烦。还有一个办法就是指定函数输入参数的类型。我们可以加上代码:
from ctypes import c_char, c_char_p
strchr = libc.strchr
strchr.argtypes = [c_char_p, c_char]
print strchr('abcdef', 'd')
函数的argtypes
属性就可以指定传入参数的类型。这里,第一个参数是字符指针,也就是C中的字符串,第二个是字符。
我们再来执行下程序,奇怪,虽然有返回了,但一直是一个长整型数值,为什么呢?了解strchr
的朋友们应该知道,这个函数返回的是char *
类型,它是一个字符指针,所以你在Python中获取的那个数值,就是指针的地址。那要怎么把指针转为字符串呢?也很简单,通过函数的restype
属性指定返回值类型即可。完整的程序如下:
from ctypes import cdll, c_char, c_char_p
libc = cdll.LoadLibrary('libc.so.6') # Load standard C library on Linux
strchr = libc.strchr
strchr.argtypes = [c_char_p, c_char]
strchr.restype = c_char_p
substr = strchr('abcdef', 'd')
if substr:
print substr
关于ctypes库的更详细内容可以参考Python官方文档。
文中的示例代码可以从这里下载