初用C/C++扩展Python,提高性能

2023-04-04,,

    前段时间写了两篇文章介绍如何提高Python的运行效率,一篇是从python语言本身的角度去介绍的,另一篇是从解释器角度(利用PyPy),有兴趣的可以找着看看。从另外一个角度来介绍如何提高python运行效率,那就是利用c/c++来扩展python提高性能。我们知道python官方网站上下载的python解释器源码是用c语言编写的,所以,也可以利用c/c++来扩展它,以获得较优的执行性能。Python提供了API接口,是我们很方便的能进行扩展,所有这些API都包含在Python.h的头文件里,在编写c代码时引入该头文件即可。下面来讲讲我初用c/c++扩展python的经历吧。申明所有系统Ubuntu 16.04 LTS。

一)Ubuntu下如何编译.c文件

    说实话,进行这部分学习时,第一困扰我的问题就是不知道怎么在Ubuntu下编译.c文件,以前在大学的时候,用的是Windows系统,安装了visual studio 2010,代码编写,编译都很方便。没有在Ubuntu下用c语言开发,所以这部分知道还不太了解,好在网上资料非常丰富,很快就能找到相应的资料。其实Ubuntu系统自带c语言编译器,那就是gcc。不信你可以打开终端,输入命名:gcc --version,显示如下信息:

如果没有的话,你也可自己安装,安装命令如下:

sudo apt-get install gcc

建议还要安装一个build-essentiall包,作用是提供编译程序必须软件包的列表信息,也就是说 编译程序有了这个软件包它才知道 头文件在哪 ,才知道库函数在哪,还会下载依赖的软件包 , 最后才组成一个开发环境。

好了,现在写个简单的.c文件吧。

打开终端,输入命令vim hello.c,当然,你也可以用gedit来编辑代码,只要文件扩展名不搞错了就行。代码如下:

#include<stdio.h>
int main()
{
int i;
for(i=0;i<3;i++)
printf("hello,gcc!\n");
}

然后保存文件并退出。编译成可执行文件,命令如下:

gcc hello.c -o hello

然后输入./hello执行该文件。效果如下图。

这里只是简单的讲了一下linux的gcc的使用方法,关于它更多的用法可以在网上查资料,在此不做过多介绍。

(二)使用C/C++扩展Python

1.先利用Python提供的接口,编写一个汉语一定功能.c文件,比如判断一个数是不是素数。在这里文件名为test.c,使用c语言实现后再使用Python进行包装。代码如下:

#include<Python.h>
static PyObject *pr_isprime(PyObject *self,PyObject *args){
    int n,num;
    if(!PyArg_ParseTuple(args,"i",&num))
        return NULL;
    if(num<1){
        return Py_BuildValue("i",0);
    }
    n=num-1;
    while(n>1){
        if(num%n==0)
            return Py_BuildValue("i",0);
            n--;
    }
    return Py_BuildValue("i",1);
}

static PyMethodDef PrMethods[]={
    {"isPrime",pr_isprime,METH_VARARGS,"check if an input numbe is prime or not."},
    {NULL,NULL,0,NULL}
};

void initpr(void){
    (void) Py_InitModule("pr",PrMethods);
}

PyObject是Python对象机制的基础,所有得对象都拥有一些相同的内容,而这些内容在PyObject中定义。

上面的代码包括三部分:

一是导出函数,C模块对外暴露的接口函数pr_isprime,带有self和args两个参数,其中参数args中包含了python解释器要传递给c函数的所有参数,通常使用PyArg_ParseTuple()来获得这些参数。

二是初始化函数,以便python解释器能够对模块进行初始化,初始化时要以init开头,如initx。

第三是方法列表,提供给外部的python程序使用的一个C模块函数名称映射表PrMethods。它是一个PyMethodDef结构体,其中成员依次表示方法名、导出函数、参数传递方式和方法描述。

参数传递方式一般设置为METH_VARARGS,如果想传递关键字参数,你可以让它与MRTH_KEYWORDS进行或运算;如果不想接受任何参数,可以用METH_NOARGS,该结构体必须以{NULL,NULL,0,NULL}所表示的一条空记录结尾。

至于为什么这样做,可能与Python.h里面的函数有关,该部分我还未仔细研究,所以这篇文章也叫题目用了“初用”,如果有以后弄清楚了,我会“再用”,与大家分享学习经验。

2.编写setup.py脚本

代码如下:

rom distutils.core import setup,Extension
module = Extension('pr',sources=['test.c'])
setup(name='Pr test',version = '1.0',ext_modules=[module])

 distutils包是可以用来在Python环境中构建和安装额外的模块。新的模块可以是纯Python的,也可以是用C/C++写的扩展模块,或者可以是Python包,包中包含了由C和Python编写的模块。在此不做过多介绍,有兴趣可以在网上找资料了解下。

3.使用python setup.py build进行编译,系统会自动在当前目录生成一个build子目录,里面包含一个.so文件、.o文件。看看我的操作结果吧

执行python setup.py build命令后,系统确实在当前目录生成了一个build子目录,并且有一个.so文件、.o文件。

4.将生成的.so文件复制到/usr/local/lib/python2.7/dist-packages下,或者将.so文件所在目录路径添加到sys.path中,然后就可以使用该C扩展模块了。如下图:

(三)简单测试下性能

代码如下:

#coding=utf-8
import pr
import datetime
def is_prime(n):
    if n <= 1:#
        return False
    else:
        a = n - 1
        while(a > 1):
            if n % a == 0:
                return True
            a = a - 1
        return False
t1 = datetime.datetime.now()
is_prime(8888888)
t2 = datetime.datetime.now()
pr.isPrime(8888888)
t3 = datetime.datetime.now()
print t3 - t2,t2 - t1

其中is_prime()是用python写的判断一个正数是不是素数。它的循环算法与pr模块里面的isPrime()是一样的,但是效率却有很大差异,看看下图运行结果。

很显然用c扩展的那个包里的函数计算速度快而且是块近20倍,数值越大,速度上的差距越大,有兴趣的话,可以动手试下。当然这只是一个定性分析。