Py学习  »  Python

Python与C语言扩展-让你的程序比别人快百倍

天澄 • 4 年前 • 377 次点击  
阅读 97

Python与C语言扩展-让你的程序比别人快百倍

Python 的性能问题一直开发者吐槽的诟病,但 Python 语言的语法简洁和快速开发又让开发者深受喜爱。如何提高 Python 程序运行性能,一直是 Python 开发者的课题。

本篇文章介绍一种方法,通过 Python 的 C扩展程序,来提高运行性能。Python 最受欢迎的解释器是 CPython,是C语言编写。所以 Python 和 C语言本身有很好的兼容性,二者也可以互相调用。

接下来就分享一下如何用Python调用C程序。

本文的环境: mac + Python3。 程序以 斐波拉契 算法程序为例。 示例完整代码地址: github.com/fuzctc/tc-p…

1. Python/C API

首先介绍最基本的方式,通过Python/C API来实现。

Python extension module 是Python官方提供Python以外的语言建立且能够让Python调用的module,官方文档地址: docs.python.org/3/extending… ,建议大家在写C扩展程序的时候,都拜读一下。

首先给大家展示一下 Python 版的斐波拉契:

def fib_recursive(n):
    if n < 2:
        return n
    return fib_recursive(n - 1) + fib_recursive(n - 2)
    
start_ts = time.time()
print(fib_recursive(35))
print(time.time() - start_ts)

# 运行结果:
# 9227465
# 3.8501219749450684
复制代码

如果将此程序改为 C 语言程序,命名为 speedup_fib.c,如何进行改写,需要如下步骤:

1.1 引入 Python.h

Python/C API 是C语言里建立 Python extension module 的煤介,必须先引入 Python.h 头文件。

// content of speedup_fib.c
#include <Python.h>

long long _fib(long long n){
    if(n < 2)
        return n;
    else
        return _fib(n-1) + _fib(n-2);
};
复制代码

引入 Python.h,写一个 C 语言版本的 斐波拉契。

1.2 包装 Function

在 Python 中一切皆对象,对应到C语言中是 PyObject,所以要将原来的function包装一下,让参数和返回值均为 PyObject

官方提供了三种参数形式:

  • (PyObject *self)
  • (PyObject *self, PyObject *args)
  • (PyObject *self, PyObject *args, PyObject *kwargs)

其中 args 表示 positional arguments, kwargs 是 keyword arguments.

要把 Python 中的这些 arguments 转为 自己定义的 C 语言参数的话,还需要调用几个API,具体可以参考官方文档 docs.python.org/3/c-api/arg… 截图如下。

常用的就是2个。

  • int PyArg_ParseTuple(PyObject *args, const char *format, ...)
  • int PyArg_ParseTupleAndKeywords(PyObject *args, PyObject *kw, const char *format, char *keywords[], ...)

第一个处理常规带 positional arguments ,第二个处理带 positional arguments 和 keyword arguments.

其中 format 是格式化参数类型,比如长整型或者字符串等,具体参考官方文档: docs.python.org/2.0/ext/par…

因为 斐波拉契 程序没有键值对参数,所以采用第一个函数,且参数需要格式化为长整型,查询文档长整型是 l 表示。

根据上述解释后,最终包装后的程序为:

// content of speedup_fib.c
static PyObject *fib(PyObject *self, PyObject *args) {

   long long n; // 定义 参数

   long long res; // 定义返回值
   
   // 将参数进行包装,并格式化为长整型l,如果包装失败,则返回NULL.
   if (!PyArg_ParseTuple(args, "l", &n))
       return NULL;
   // 调用C语言版本的斐波拉契,同时传入包装好的参数n
   res = _fib(n);
   // 将返回值用  Py_BuildValue 包成 PyObject 传给 Python
   return Py_BuildValue("l", res);
};
复制代码

包装的 斐波拉契 函数定义为fib,最后返回值也需要进行包装,将C语言的返回值打包成 PyObject

1.3 声明 Module Methods 列表

把 module 的函数进行一一包装后,要建立这个module的method列表,目的就是声明每一个包装函数和C语言函数的对应关系,以及函数是以哪种形式进行传入,格式为:

{name, method, flags, doc}
即 {名称,包装函数,哪种argument形式, 描述}
复制代码

flags 的标识可以参考官方文档: docs.python.org/3/c-api/str…

比较多的就是 METH_VARARGS 和 METH_KEYWORDS,分别对应 args 和 keywords。

所以根据上述描述,定义的Module Methods 为:

// content of speedup_fib.c
static PyMethodDef SpeedupFibMethods[] = {
    {"speedup_fib", (PyCFunction) fib, METH_VARARGS, "fast fib"},
    {NULL, NULL, 0, NULL} // 以 NULL 作结
};
复制代码

1.4 定义 Module 的结构

建立了 Module Methods 之后,还需要定义Module的结构,需要这些信息去创建一个 module object。格式为:

{base, name, doc, size, module methods 表}
即 {PyModuleDef_HEAD_INIT, 名字, 描述, 分配内存大小, module 方法列表}
复制代码

相关定义可以参考官方文档: docs.python.org/3/c-api/mod…

根据上面描述,定义 Module 结构函数为:

// content of speedup_fib.c


    

static struct PyModuleDef speedup_fib_module = {
    PyModuleDef_HEAD_INIT,
    "speedup_fib",
    "A module containing methods with faster fib.",
    -1, // global state
    SpeedupFibMethods
};
复制代码

1.5 定义Module Initialization Method

接下来定义 Module Initialization Method,目的是根据module结构信息去创建一个module object,而这个module object就可以供Python调用。

需要注意Module Initialization Method必须以PyInit_开头。

// content of speedup_fib.c
PyMODINIT_FUNC PyInit_speedup_fib() {
  return PyModule_Create(&speedup_fib_module);
}
复制代码

1.6 建立 Extension Module

上面5步已经把 speedup_fib.c 中的代码部分写完,接下来需要创建一个 setup.py,通过 Distutils 模块将C语言模块创建出来。

# content of setup.py
from distutils.core import setup, Extension
speedup_fib_module = Extension('speedup_fib', sources=['speedup_fib.c'])

setup(
    name='SpeedupFib',
    description='A package containing modules for speeding up fib.',
    ext_modules=[speedup_fib_module],
)
复制代码

首先指定哪个 C 程序中的 哪个方法,然后调用 setup 建立扩展文件。

通过以下命令:

python3 setup.py build_ext --inplace
复制代码

会在当前文件夹建立一个 speedup_fib.cpython-37m-darwin.so 文件,接下来就可以直接在Python中进行调用。

测试一下:

from speedup_fib import speedup_fib
start_ts = time.time()
print(speedup_fib(35))
print(time.time() - start_ts)

# 运行结果:
# 9227465
# 0.054654836654663086
复制代码

对比之前的Python程序,速度快了80倍,如果n更大,速度差距会更大。

2. ctypes

如果觉得第一种方式过于繁琐,接下来介绍 ctypes,ctypes是Python提供的一个libray,可以让Python进入外部的dynamic-link library (DLL) 或 shared library 来调用其中的函数。

这样不再需要关注Python与C相关的API,专注写C函数即可。

2.1 编写C语言版程序

这一步在任何方式中都不能省,首先还是编写C语言版本的程序。

// content of speedup_fib.c
long long fib(long long n){
    if(n < 2)
        return n;
    else
        return fib(n-1) + fib(n-2);
};
复制代码

是不是很简单,只需要专注写函数,连头文件都不需要。

2.2 建立 Shared Library

这一步需要用到 gcc 工具,如果没有的,需要先安装。

gcc -shared -fPIC speedup_fib.c -o speedup_fib.so
复制代码

通过上述命令将 speedup_fib.c 产生一个 speedup_fib.so 文件。

2.3 引入 Library

接下来就简单了,只需要 ctypes 提高的方法,引入 speedup_fib.so 文件,然后就可以进行 Python 运行了。

# content of fib.py

from ctypes import *
func = cdll.LoadLibrary('./speedup_fib.so')

start_ts = time.time()
print(func.fib(35))
print(time.time() - start_ts)
复制代码

运行上述 fib.py 文件 得到结果:

9227465
0.06056809425354004
复制代码

3. SWIG

SWIG (Simplified Wrapper and Interface Generator) 是更加通用和全面的工具,支持 Python、Perl、Ruby等多种语言.

首先需要先安装 SWIG,如果是 mac 环境 直接 brew install swig即可,window环境参考官网: www.swig.org/Doc3.0/Pref…

3.1 创建C语言程序版本

这一步不可少,但是在swig里需要命令为头文件.h。

// content of speedup_fib.h
long long fib(long long n){
    if(n < 2)
        return n;
    else
        return fib(n-1) + fib(n-2);
};
复制代码

3.2 建立 Interface File

接下来建立接口文件,也可以说是描述接口的档案,习惯命名为 *.i or *.swg。

接下来定义一个 speedup_fib.i

// content of speedup_fib.i

/* 定义 module名称 */
%module speedup_fib

/*导入定义的 speedup_fib.h*/
%{
#include "speedup_fib.h"
%}
/* 告诉 SWIG 定义的 function 或 variable */
long long fib(long long n);
复制代码

上述定义的 speedup_fib.i中,第一步定义Module名称,第二步引入定义的 speedup_fib.h,里面swig会调用里面的函数,第三步是声明函数。

3.3 产生 Wrapper File

通过 SWIG 将 Interface File 生成 extension module 的 speedup_fib.py 和 wrapper file 的 speedup_fib_wrap.c。

命令如下:

swig -python speedup_fib.i
复制代码

当前文件夹会多2个文件: speedup_fib.py 和 speedup_fib_wrap.c。

3.4 建立 Shared Library

这一步跟 Python/C API 创建扩展模块,用用 setup.py 和 Distutils 建立 shared library:

# content of setup.py

from distutils.core import setup, Extension

# Extension module name 要有底线前缀
speedup_fib_module = Extension('_speedup_fib', sources=['speedup_fib_wrap.c'])
setup(
    name='SpeedupFib',
    description='A package containing modules for speeding up performance.',
    ext_modules=[speedup_fib_module],
)
复制代码

注意 Extension module name 要有底线前缀。

接下来下面命令:

python3 setup.py build_ext --inplace
复制代码

会在当前文件夹建立一个 _speedup_fib.cpython-37m-darwin.so 文件,接下来就可以直接在Python中进行调用。

# content of fib.py

from speedup_fib import fib

start_ts = time.time()
print(fib(35))
print(time.time() - start_ts)
复制代码

运行 fib.py, 运行结果为:

9227465
0.05449485778808594
复制代码

这一篇文章就到这里,通过上面三种方法,可以通过C扩展程序提高Python程序运行性能。示例中完整代码地址: github.com/fuzctc/tc-p…

有关更多提高 Python程序性能文章,请关注公众号:

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/34609
 
377 次点击