HackPython致力于有趣有价值的编程教学
当Python面临运算密集型任务时,其速度总是显得力不从心。要提升Python代码运行速度有多种方法,如ctypes、cython、CFFI等,本篇文章主要从ctypes方面介绍如何提升Python的运行速度?。
ctypes是Python的内置库,利用ctypes可以调用C/C++编译成的so或dll文件(so存在linux/MacOS中,dll存在于windows)?,简单而言,就是将计算压力较大的逻辑利用C/C++来实现,然后编译成so或dll文件,再利用ctypes加载进Python,从而将计算压力大、耗时较长的逻辑交于C/C++去执行。如Numpy、Pandas这些库其底层其实都是C/C++来实现的?。
下面代码的运行环境为:MacOS 、 Python3.7.3
为了对比出使用ctypes后程序运行速度的变化,先使用纯Python代码实现一段逻辑,然后再利用C语言去实现相同的逻辑?。
这里为了模仿运算密集任务,实现一段逻辑用于计算一个集合中点与点之间的距离以及实现一个操作字符串的逻辑,具体代码如下:
import random
import time
# 点
class Point():
def __init__(self, x, y):
self.x = x
self.y = y
class Test():
def __init__(self, string, nb):
self.string = string
self.points = []
# 初始化点集合
for i in range(nb):
self.points.append(Point(random.random(), random.random()))
self.distances = []
# 增量字符串
def increment_string(self, n):
tmp = ""
# 每个字符做一次偏移
for c in self.string:
tmp += chr(ord(c) + n)
self.string = tmp
# 这个函数计算列表中每个点之间的距离
def distance_between_points(self):
for i, a in enumerate(self.points):
for b in self.points:
# 距离公式
self.distances.append(((b.x - a.x) ** 2 + (b.y - b.x) ** 2) ** 0.5)
if __name__ == '__main__':
start_time = time.time()
test = Test("A nice sentence to test.", 10000)
test.increment_string(-5) # 偏移字符串中的每个字符
test.distance_between_points() # 计算集合中点与点之间的距离
print('pure python run time:%s'%str(time.time()-start_time))
上述代码中,定义了Point类型,其中有两个属性,分别是x与y,用于表示点在坐标系中的位置?,然后定义了Test类,其中的increment_string()方法用于操作字符串,主要逻辑就是循环处理字符串中的每个字符,首先通过ord()方法将字符转为unicode数值,然后加上对应的偏移n,接着在通过chr()方法将数值转换会对应的字符?。
此外还实现了distance_between_points()方法,该方法的主要逻辑就是利用双层for循环,计算集合中每个点与其他点的距离。使用时,创建了10000个点进行程序运行时长的测试?。
多次执行这份代码,其运行时间大约在39.4左右?
python 1.py
pure python run time:39.431304931640625
要使用ctypes,首先就要将耗时部分的逻辑通过C语言实现,并将其编译成so或dll文件,因为我使用的是MacOS,所以这里会将其编译成so文件?,先来看一下上述逻辑通过C语言实现的具体代码,如下:
#include
#include
// 点结构
typedef struct s_point
{
double x;
double y;
} t_point;
typedef struct s_test
{
char *sentence; // 句子
int nb_points;
t_point *points; // 点
double *distances; // 两点距离,指针
} t_test;
// 增量字符串
char *increment_string(char *str, int n)
{
for (int i = 0; str[i]; i++)
// 每个字符做一次偏移
str[i] = str[i] + n;
return (str);
}
// 随机生成点集合
void generate_points(t_test *test, int nb)
{
// calloc () 函数用来动态地分配内存空间并初始化为 0
// 其实就是初始化变量,为其分配内存空间
t_point *points = calloc(nb + 1, sizeof(t_point));
for (int i = 0; i < nb; i++)
{
points[i].x = rand();
points[i].y = rand();
}
// 将结构地址赋值给指针
test->points = points;
test->nb_points = nb;
}
// 计算集合中点的距离
void distance_between_points(t_test *test)
{
int nb = test->nb_points;
// 创建变量空间
double *distances = calloc(nb * nb + 1, sizeof(double));
for (int i = 0; i < nb; i++)
for (int j = 0; j < nb; j++)
// sqrt 计算平方根
distances[i * nb + j] = sqrt((test->points[j].x - test->points[i].x) * (test->points[j].x - test->points[i].x) + (test->points[j].y - test->points[i].y) * (test->points[j].y - test->points[i].y));
test->distances = distances;
}
其中具体的逻辑不再解释,可以看注释理解其中的细节,通过C语言实现后,接着就可以通过gcc来编译C语言源文件,将其编译成so文件?,命令如下:
// 生成 .o 文件
gcc -c fastc.c
// 利用 .o 文件生成so文件
gcc -shared -fPIC -o fastc.so fastc.o
获得了fastc.so文件后,接着就可以利用ctypes将其调用并直接使用其中的方法了,需要注意的是「Windows系统体系与Linux/MacOS不同,ctypes使用方式会有差异」?,至于ctypes的具体用法,后面会通过单独的文章进行讨论。
ctypes使用fastc.so的代码如下:
import ctypes
from ctypes import *
from ctypes.util import find_library
import time
# 定义结构,继承自ctypes.Structure,与C语言中定义的结构对应
class Point(ctypes.Structure):
_fields_ = [('x', ctypes.c_double), ('y', ctypes.c_double)]
class Test(ctypes.Structure):
_fields_ = [
('sentence', ctypes.c_char_p),
('nb_points', ctypes.c_int),
('points', ctypes.POINTER(Point)),
('distances', ctypes.POINTER(c_double)),
]
# Lib C functions
_libc = ctypes.CDLL(find_library('c'))
_libc.free.argtypes = [ctypes.c_void_p]
_libc.free.restype = ctypes.c_void_p
# Lib shared functions
_libblog = ctypes.CDLL("./fastc.so")
_libblog.increment_string.argtypes = [ctypes.c_char_p, ctypes.c_int]
_libblog.increment_string.restype = ctypes.c_char_p
_libblog.generate_points.argtypes = [ctypes.POINTER(Test), ctypes.c_int]
_libblog.distance_between_points.argtypes = [ctypes.POINTER(Test)]
if __name__ == '__main__':
start_time = time.time()
# 创建
test = {}
test['sentence'] = "A nice sentence to test.".encode('utf-8')
test['nb_points'] = 0
test['points'] = None
test['distances'] = None
c_test = Test(**test)
ptr_test = ctypes.pointer(c_test)
# 调用so文件中的c语言方法
_libblog.generate_points(ptr_test, 10000)
ptr_test.contents.sentence = _libblog.increment_string(ptr_test.contents.sentence, -5)
_libblog.distance_between_points(ptr_test)
_libc.free(ptr_test.contents.points)
_libc.free(ptr_test.contents.distances)
print('ctypes run time: %s'%str(time.time() - start_time))
多次执行这份代码,其运行时间大约在1.2左右?
python 2.py
ctypes run time: 1.2614238262176514
相比于纯Python实现的代码快了30倍有余?
本节简单的讨论了如何利用ctypes与C/C++来提升Python运行速度,有人可能会提及使用asyncio异步的方式来提升Python运行速度,但这种方式只能提高Python在IO密集型任务中的运行速度,对于运算密集型的任务效果并不理想?,最后欢迎学习 HackPython 的教学课程并感觉您的阅读与支持。
??