Python简单明了,易于理解,但这样的优点带来的是速度上的缓慢。在这样的背景下,我们想在Python程序中插入C,已进行一些耗时的计算任务,以实现速度的提升。本文主要介绍ctypes
库的使用方法,为后面的工作提供参考。
ctypes
是 Python 的外部函数库。它提供了与 C 兼容的数据类型,并允许调用DLL或共享库中的函数。可使用该模块以纯 Python 形式对这些库进行封装。
动态链接库是一种不可执行的二进制程序文件,它允许程序共享执行特殊任务所必需的代码和其他资源。
首先,我们需要有Python解释器,ctypes
是Python的标准库;此外,由于我们需要生成动态链接库,一个C编译器也是必须的。为了简单期间,笔者选择在Linux
平台上实验后面的代码。
sudo apt install python3
sudo apt install gcc
sudo apt install g++
先写一个简单的C文件:
// test.c
#include <stdio.h>
int print_hello(const char* n) {
printf("hello %s!\n", n);
return 0;
}
将其编译成动态链接库文件:
gcc -fPIC -shared -o test.so test.c
这里-fPIC
是指生成位置无关的代码,则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的;-shared
表明产生共享库;-o test.so
表明生成的目标文件名为test.so
。
Windows系统中的动态链接库文件后缀名为
dll
,Linux下为so
。
我们用ctypes
加载动态链接库,然后就可以调用其中的函数了:
from ctypes import *
lib = CDLL('./test.so')
lib.print_hello(b'world')
输出hello world!
。
我们这里输入参数是
b'world'
而不是简单的world
,这是因为Python3的字符串的编码语言用的是unicode编码,由于Python的字符串类型是str,在内存中以Unicode表示,一个字符对应若干字节。而在C中,字符串是以一个字符一个字节,所以需要进行转换。
由于我们在C程序中包括了stdio.h
,我们甚至可以调用printf
等库函数:
lib.printf(b'hello %s, a number %d', b'world', c_int(12))
等价于
printf("hello %s, a number %d", "world", 12);
我们来比较下纯Python和C+Python下运行速度的差别,在test.c
中定义C函数add_test
,用于测试两个数的加法:
int add(int x, int y) {
return x + y;
}
void add_test(int N) {
for(int i=0; i < N; i++) {
add(i, i + 1);
}
}
对应的在Python
中定义add
函数,计算多次循环时间:
from ctypes import *
from time import time
N = 100000
lib = CDLL("test.so")
def add(x, y):
return x + y
def add_test(N):
for i in range(N):
add(i, i + 1)
# Python测试
t = time()
add_test(N)
t_python = time() - t
# C测试
t = time()
lib.add_test(c_int(N))
t_c = time() - t
print("{} times add:\nPython : {}\n C : {}".format(N, t_python, t_c))
执行
gcc -fPIC -shared -o test.so test.c && python3 test.py
输出
100000 times add:
Python : 0.014992952346801758
C : 0.0004227161407470703
可以发现,C和Python的混合编程可以大幅度提高程序的速度。
数据类型是沟通两种语言的基石,ctypes
下定义了与C
兼容的数据类型。
ctypes 类型 | C 类型 | Python 类型 |
---|---|---|
c_bool |
_Bool |
bool |
c_char |
char |
单字符字节对象 |
c_wchar |
wchar_t |
单字符字符串 |
c_byte |
char |
整型 |
c_ubyte |
unsigned char |
整型 |
c_short |
short |
整型 |
c_ushort |
unsigned short |
整型 |
c_int |
int |
整型 |
c_uint |
unsigned int |
整型 |
c_long |
long |
整型 |
c_ulong |
unsigned long |
整型 |
c_longlong |
__int64 或 long long |
整型 |
c_ulonglong |
unsigned __int64 或 unsigned long long |
整型 |
c_size_t |
size_t |
整型 |
c_ssize_t |
ssize_t 或 Py_ssize_t |
整型 |
c_float |
float |
浮点数 |
c_double |
double |
浮点数 |
c_longdouble |
long double |
浮点数 |
c_char_p |
char * (以 NUL 结尾) |
字节串对象或 None |
c_wchar_p |
wchar_t * (以 NUL 结尾) |
字符串或 None |
c_void_p |
void * |
int 或 None |
我们要明确,Python的数据类型和C的数据类型有很大不同,进一步说,Python中的数据类型,比如
int
、float
等,是一种“类”,是面向对象式且抽象的,而C中的数据,往往具象到内存上,比如int
类型就对应内存上的4字节。
在前面我们可以看到,由于Python中的数据类型与C中的差别,我们无法将Python中的数据直接作为C函数的参数,而是要进行预处理:
'''
C中函数:
int add(int x, int y) {
return x + y;
}
'''
lib.add(1, 2) # 报错 ctypes.ArgumentError
lib.add(c_int(1), c_int(2)) # 正确写法
显然,当函数使用次数增加时,这样的写法是不利于书写和阅读的。在ctypes
中,我们可以指定C
函数的参数类型,这样,我们就可以直接将Python数据类型作为C函数的参数,因为编译器在中间进行了隐式转换:
c_add = lib.add
c_add.argtypes = [c_int, c_int]
c_add(1, 2) # 可行,不会报错
默认情况下,函数的返回值是int
类型:
double add(double a, double b) {
double c = a + b;
return c;
}
在Python
中调用函数:
x, y = 2.1, 3.2
c_add = lib.add
c_add.argtypes = [c_double, c_double]
ret = c_add(x, y)
print(ret)
输出2
,这显然是错误的。在ctypes
中,我们往往需要指定返回值类型:
c_add.restype = c_double
将该语句插入到函数调用前,得到正确答案5.3
.
通过ctypes.byref
实现函数指针参数的传递(可以把它理解为C中的取地址符&
),我们自制一个测试函数:
double ref_test(double *a, double *b) {
// 获取两个指针变量指向的值之和,并改变指针指向的值
double c = *a + *b;
*a = 1, *b = 2;
return c;
}
在Python
中调用:
# 省略import和dll的加载
x, y = c_double(10), c_double(20)
p_add = lib.p_add
p_add.restype = c_double # 不要忘记确定返回值
print(x.value, y.value)
ret = p_add(byref(x), byref(y))
print(x.value, y.value, ret)
调用前,x
和y
的值分别是10和20,而调用后,我们通过指针修改了变量值,因此输出
1.0 2.0 30.0
在ctypes
中,我们可以像C
中typedef
一样定义类型,用一个类型乘以一个正数创建数组类型,比如
ten_ele_arr = c_int * 10 # 类型:10元素整形数组类
array = ten_ele_arr() # 实例:一个10元素数组
for ele in array:
print(ele, end=' ') # 默认元素全0
我们也可以用具体的元素初始化数组:
array = ten_ele_arr(1, 2, 3)
for ele in array:
print(ele, end=' ') # 输出1 2 3 0 0 0 0 0 0 0
我们在这里介绍了ctypes
的用法,如何实现Python
与C
的交互,以实现运行速度的提升。