使用 CFFI/Cython 编写 Python 扩展
除了使用并发编程,还有三种方法可以提高 Python 代码的执行效率。
1.使用 PyPy。PyPy 使用即时编译器(Just in Time compiler,简称 JIT)编译代码,对于长期执行的程序能明显提高效率,如 Web 服务。
2.通过第三方工具让 Python 程序调用 C/C++代码。目前最流行的方式有以下 3 种:
- SWIG。SWIG 可以把 C/C++的代码封装成 Python 库,供 Python 调用。使用 SWIG 可以有效利用脚本语言的开发效率和 C/C++的运行效率。
- Boost.Python。Boost.Python 是 Boost 中的一个组件,使用它能够大大简化用 C++为 Python 写扩展库的步骤,提高开发效率。
- CFFI。CFFI 实现类似 ctypes 的访问 C 库并调用其函数的功能。PyPy、cryptog-raphy、PIL 等都使用了它。
3.改用 Cython 编写代码。
使用 CFFI
开发者只要会 C 和 Python 就可以使用 CFFI,而且大部分场景下直接从头文件或者文档拷贝声明即可。
我们先安装它:
> pip install cffi
CFFI 有 ABI 和 API 共两种模式,每种模式下又包含 in-line 和 out-of-line 这两种编译模式。in-line 表示即时编译使用,常用来做效果测试;out-of-line 表示离线编译后调用,生产环境都使用这种模式。我们在 C 标准库参考教程(http://bit.ly/1OsuDLd )上找到 ceil 函数来感受下这 4 种模式。
1.ABI 的 in-line 模式。ABI 模式模式不需要任何 C 编译器:
In : from cffi import FFI
In : ffi=FFI()
In : ffi.cdef('double ceil(double x);')
In : C=ffi.dlopen(None) # 加载动态链接库,使用 None 表示加载 C 标准库
In : val1=ffi.cast('float', 10.9)
In : C.ceil(val1)
Out: 11.0
其中 cdef 方法中的内容是直接从文档中拷贝的,但是要确保行尾有分号。
2.ABI 的 out-of-line 模式。
from cffi import FFI
ffi=FFI()
ffi.set_source('_abi_out', None)
ffi.cdef('double ceil(double x);')
ffi.compile()
执行 ffi.compile 方法后会生成_abi_out.py 文件,接下来的操作都基于_abi_out.py:
from _abi_out import ffi as ffi_
lib=ffi_.dlopen(None)
print lib.ceil(ffi.cast('float', 10.9))
3.API 的 in-line 模式。在线写 C 代码即可:
In : from cffi import FFI
In : ffi=FFI()
In : ffi.cdef('double ceil(double x);')
In : lib=ffi.verify('double ceil(double x);')
In : lib.ceil(10.9)
Out: 11.0
4.API 的 out-of-line 模式。ABI 的 out-of-line 生成的是 Python 代码,而 API 使用了编译器,会生成.c、.o 和.so 文件:
from cffi import FFI
ffi=FFI()
ffi.set_source(
'_api_out',
'''
#include <math.h>
'''
)
ffi.cdef('double ceil(double x);')
ffi.compile()
from_api_out import lib
print lib.ceil(10.9)
能产生这样的区别,原因在于 set_source 方法的第二个参数是不是 None。
上面的例子都是使用 C 标准库的函数,现在演示一个完整的例子。首先创建一个头文件(board.h),文件内容主要是函数、结构声明、常量定义等:
typedef struct{
int p_id;
wchar_t*p_name;
} board_t;
board_t*create(int id, const wchar_t*name);
void board_destroy(board_t*p);
其中定义了一个叫作 board_t 的结构体,它包含 p_id 和 p_name 两个字段;还定义了创建和销毁结构体的函数。
然后创建一个源文件(board.c,CFFI 编译后会生成完整的.c 源文件),.c 文件存放函数定义,board.c 中包含了创建和销毁结构体的函数:
#include<stdlib.h>
#include<wchar.h>
board_t*create(int id, const wchar_t*name){
board_t*p=malloc(sizeof(board_t));
if (!p)
return NULL;
p->p_id=id;
p->p_name=wcsdup(name);
return p;
}
void board_destroy(board_t*p){
if (p->p_name)
free(p->p_name);
free(p);
}
使用 API 的 out-of-line 模式来创建_board.so(build_board.py):
import os
from cffi import FFI
ffi=FFI()
here=os.path.dirname(__file__)
with open(os.path.join(here, 'board.h')) as f:
header=f.read().strip()
with open(os.path.join(here, 'board.c')) as f:
source=f.read().strip()
ffi.set_source('_board', '\n'.join([header, '', source]))
ffi.cdef(header)
ffi.compile()
运行之后,可以看到已经在当前目录下生成了动态链接库_board.so。使用它:
from_board import ffi, lib
class Board(object):
def __init__(self, id, name):
p=lib.create(id, name)
if p==ffi.NULL:
raise MemoryError('Could not allocate board')
self._p=ffi.gc(p, lib.board_destroy)
@property
def id(self):
return self._p.p_id
@property
def name(self):
return ffi.string(self._p.p_name)
这样就能使用 Board 类了:
In : from board import Board In : board=Board(1, u'board_1') In : board.id, board.name Out: (1, u'board_1')
使用 Cython
Cython 在本质上是包含 C 数据类型的 Python,几乎所有 Python 代码都是合法的 Cython 代码,Cython 能够把稍加修改的 Python 代码编译成 C,速度却能提升几倍到几百倍,这是并发程序很难达到的。
我们先安装它:
> pip install Cython
编辑距离(Edit Distance),又称 Levenshtein 距离,是指两个字符串之间由一个转成另一个所需的最少编辑操作次数。编辑距离越小,两个字符串的相似度越大。它也是字符串模糊匹配库 FuzzyWuzzy 的依赖。本节我们将对比纯 Python、只使用 Cython 编译、使用 Cython 语法编写这三种方式在效率上的提升。
我们从维基百科找到一个 Levenshtein 距离的 Python 实现(levenshtein_p.py):
def levenshtein(s, t):
if s==t:
return 0
elif len(s)==0:
return len(t)
elif len(t)==0:
return len(s)
v0=[None]*(len(t)+1)
v1=[None]*(len(t)+1)
for i in range(len(v0)):
v0[i]=i
for i in range(len(s)):
v1[0]=i+1
for j in range(len(t)):
cost=0 if s[i]==t[j] else 1
v1[j+1]=min(v1[j]+1, v0[j+1]+1, v0[j]+cost)
for j in range(len(v0)):
v0[j]=v1[j]
return v1[len(t)]
不做任何修改,将上述代码拷贝出来,命名为 levenshtein_c.pyx,再创建一个 setup.py 文件编译它:
from distutils.core import setup
from Cython.Build import cythonize
setup(
name='levenshtein_c',
ext_modules=cythonize('levenshtein_c.pyx'),
)
生成动态链接库:
> python setup_c.py build_ext --inplace
生成的动态链接库是 levenshtein_c.so。
最后再基于 levenshtein_p.py,创建 levenshtein_cy.pyx,使用 Cython 语法修改它:
def levenshtein(char*s, char*t):
cdef int i, j, cost, rs
cdef list v0, v1
...
省略的部分没有变动。可以看到,我们只是声明了参数和函数内用到的变量的类型。接下来也要通过 setup.py 编译它,生成 levenshtein.so。其中 cdef 用来声明 C 变量的类型。
现在对比一下三种方式生成的模块的效率:
In : import levenshtein_p In : import levenshtein_c In : import levenshtein_cy In : timeit-n 100 levenshtein_p.levenshtein(s1, s2) 100 loops, best of 3:3.76 ms per loop In : timeit-n 100 levenshtein_c.levenshtein(s1, s2) 100 loops, best of 3:1.7 ms per loop In : timeit-n 100 levenshtein_cy.levenshtein(s1, s2) 100 loops, best of 3:619 □s per loop
可以看到,levenshtein_c 只是通过 Cython 把代码转成 C 就让效率提升了 2 倍多,而 leven-shtein_cy 只是对 levenshtein 函数简单添加一些声明,并没有做更多的优化,就可以比 Python 版本的效率高 6 倍。
再看一个使用 C 标准库中的 math.ceil 的例子:
cdef extern from"math.h":
double ceil(double x)
cdef double f(double x):
return ceil(x)
cpdef double f2(double x):
return f(x)
extern 关键词引用的 math.h 文件中的定义,除了 cdef 声明 C 函数外,还出现了 cpdef,它是一个既能让 C 也能让 Python 调用的方式,编译之后使用一下就知道区别了:
In : import ceil In : ceil.f2(10.9) Out: 11.0 In : ceil.f(10.9) --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) <ipython-input-3-f46aedbc5205>in<module>() ----> 1 ceil.f(10.9) AttributeError: 'module' object has no attribute 'f'
使用 cdef 声明的函数 f 不是模块的一部分,但是它可以被 f2 函数调用。
上述例子中,声明 ceil 这样的常用函数是不需要找 math.h 头的,Cython 已经自带了。可以使用如下方法直接调用:
from libc.math cimport ceil
cpdef double f2(double x):
return ceil(x)
其他可用的函数声明可以查看 Cython/Includes(http://bit.ly/1XYmUrH )目录下相关的后缀名为.pxd 的文件。
嵌入模式
除了通过动态链接库作为模块被引用,还可以借用 Cython 的嵌入(Embed)模式生成可执行的二进制程序。比如下面的小程序:
import sys
name=sys.argv[1] if len(sys.argv)==2 else 'World'
print 'Hello{}'.format(name)
执行如下两步即可:
> cython --embed -o hello.c hello.py
> gcc -Os -I/usr/include/python2.7 -o hello hello.c -lpython2.7 -lpthread -lm- lutil -
ldl
现在可以执行 hello 了:
> ./hello Hello World > ./hello xiaoming Hello xiaoming
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论