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

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

本篇文章介绍一种方法,经过 Python 的 C扩展程序,来提升运行性能。Python 最受欢迎的解释器是 CPython,是C语言编写。因此 Python 和 C语言自己有很好的兼容性,两者也能够互相调用。python

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

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

1. Python/C API

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

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

首先给你们展现一下 Python 版的斐波拉契:app

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程序性能文章,请关注公众号:

相关文章
相关标签/搜索