返回介绍

使用 CFFI/Cython 编写 Python 扩展

发布于 2025-04-20 18:52:18 字数 7176 浏览 0 评论 0 收藏

除了使用并发编程,还有三种方法可以提高 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。