使用 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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论