第8章 扩展Python
C语言效率很高。但这种效率的代价是需要用户亲自进行许多低级资源管理工作。由于现在的机器性能非常强大,这种亲历亲为是得不偿失的。如果能使用一种在机器执行效率较低而用户开发效率很高的语言,则是非常明智的。Python就是这样的一种语言。
——Eric Raymond,1996年10月
本章内容:
简介和动机;
编写Python扩展;
相关主题。
本章将介绍如何编写扩展代码,并将其功能集成到Python编程环境中。首先介绍这样做的动机,接着逐步介绍如何编写扩展。需要指出的是,虽然Python扩展主要用C语言编写,且出于通用性的考虑,本节的所有示例代码都是纯C语言代码。因为C++是C语言的超集,所以读者也可以使用C++。如果读者使用Microsoft Visual Studio构建扩展,需要用到Visual C++。
8.1 简介和动机
本章第一节将介绍什么是Python扩展,并尝试说明什么情况下需要(或不需要)考虑创建一个扩展。
8.1.1 Python扩展简介
一般来说,任何可以集成或导入另一个Python脚本的代码都是一个扩展。这些新代码可以使用纯Python编写,也可以使用像C和C++这样的编译语言编写(在Jython中用Java编写扩展,在IronPython中用C#或VisualBasic.NET编写扩展)。
核心提示:在不同的平台上分别安装客户端和服务器来运行网络应用程序
这里需要提醒一下,一般来说,即使开发环境中使用了自行编译的Python 解释器, Python扩展也是通用的。手动编译和获取二进制包之间存在着微妙的关系。尽管编译比直接下载并安装二进制包要复杂一些,但是前者可以灵活地定制所使用的Python版本。如果需要创建扩展,就应该在与扩展最终执行环境相似的环境中进行开发。
本章的示例都是在基于UNIX的系统上构建的(这些系统通常自带编译器),但这里假定读者有可用的C/C++(或Java)编译器,以及针对C/C++(或Java)的Python开发环境。这两者的唯一区别仅仅是编译方法。而扩展中的实际代码可通用于任何平台上的Python环境中。
如果是在Windows平台上开发,需要用到Visual C++开发环境。Python发行包中自带了7.1版的项目文件,但也可以使用老版本的VC++。
关于构建Python扩展的更多信息请查看下面的网址。
针对PC上的C++:http://docs.python.org/extending/windows
Java/Jython:http://wiki.python.org/jython
IronPython–http://ironpython.codeplex.com
警告:尽管在相同架构下的不同计算机之间移动二进制扩展一般情况下不会出现问题,但是有时编译器或CPU之间的细微差别可能导致代码不能正常工作。
Python中一个非常好的特性是,无论是扩展还是普通Python模块,解释器与其交互方式完全相同。这样设计的目的是对导入的模块进行抽象,隐藏扩展中底层代码的实现细节。除非模块使用者搜索相应的模块文件,否则他就不会知道某个模块是使用Python编写,还是使用编译语言编写的。
8.1.2 什么情况下需要扩展Python
简要纵观软件工程的历史,编程语言过去一直都根据原始定义来使用。只能使用语言定义的功能,就无法向已有的语言添加新的功能。然而,在现今的编程环境中,可定制性编程是很吸引人的特性,它可以促进代码重用。Tcl和Python就是第一批这样可扩展的语言,这些语言能够扩展其语言本身。那么为什么需要扩展像Python这样已经很完善的语言呢?有下面几点充分的理由。
需要Python没有的额外功能:扩展Python的原因之一是需要该语言核心部分没有提供一些的新功能。使用纯Python或编译后的扩展都可以做到这一点,不过像创建新的数据类型或在已有应用中嵌入Python,就必须使用编译后的模块。
改善瓶颈性能:众所周知,由于解释型语言的代码在运行时即时转换,因此执行起来比编译语言慢。一般来说,将一段代码移到扩展中可以提升总体性能。但问题在于,如果转移到扩展中,有时代价会过高。
从性价比的角度来看,先对代码进行一些简单的性能分析,找出瓶颈所在,然后将这些瓶颈处的代码移到扩展中是个更聪明的方式。这样既能更快地获得效率提升,也不会花费太多的资源。
隐藏专有代码:创建扩展的另一个重要原因是脚本语言的缺陷。所有这样易用的语言都没有关注源码的私密性,因为这些语言的源码本身就是可执行程序。
将代码从Python中转到编译型语言中可以隐藏这些专有代码,因为后者提供的是二进制文件。编译过的文件相对来说不易进行逆向工程,这样就将源码隐藏起来了。在涉及特殊算法、加密或软件安全性时这,这就显得十分重要。
另一个保证代码私有的方式是只提供预编译的.pyc文件。在提供实际代码(.py文件)和将代码迁移到扩展这两种方法之间,这是比较好的折中。
8.1.3 什么情况下不应该扩展Python
在真正介绍如何编写扩展之前,还要了解什么情况下不应该编写扩展。这一节相当于一个告诫,否则读者会认为作者一直在为扩展Python做虚假宣传。是的,编写扩展有前面提到的那些优点,但也有一些缺点。
必须编写C/C++代码。
需要理解如何在Python和C/C++之间传递数据。
需要手动管理引用。
还有一些封装工具可以完成相同的事情,这些工具可以生成高效的 C/C++代码,但用户又无须手动编写任何 C/C++代码就可以使用这些代码。本章末尾将介绍其中一些工具。不要说我没提醒过你!下面继续……
8.2 编写Python扩展
为Python编写扩展主要涉及三个步骤。
1.创建应用代码。
2.根据样板编写封装代码。
3.编译并测试。
本节将深入了解这三个步骤。
8.2.1 创建应用代码
首先,所有需要成为扩展的代码应该组成一个独立的“库”。换句话说,要明白这些代码将作为一个Python模块存在。因此在设计函数和对象时需要考虑Python代码与C代码之间的交互和数据共享,反之亦然。
下一步,创建测试代码来保证代码的正确性。甚至可以使用 Python 风格的做法,即将main()函数放在C中作为测试程序。如果代码编译、链接并加载到一个可执行程序中(而不是共享库文件),调用这样的可执行程序能对软件库进行回归测试。下面将要介绍的扩展示例都使用这种方法。
测试用例包含两个需要引入Python环境中的C函数。一个是递归阶乘函数fac()。另一个是简单的字符串逆序函数 reverse(),主要用于“原地”逆序字符串,即在不额外分配字符串空间的情况下,逆序排列字符串中的字符。由于这些函数需要用到指针,因此需要仔细设计并调试这些C代码,以防将问题带入Python。
第1版的文件名为Extest1.c,参见示例8-1。
示例8-1 纯C版本的库(Extest1.c)
下面显示的是C函数库,需要对其进行封装以便在Python解释器中使用。main()是测试函数。
这段代码含有两个函数:fac()和 reverse(),用来实现前面所说的功能。fac()接受一个整型参数,然后递归计算结果,最后从递归的最外层返回给调用者。
最后一部分是必要的 main()函数。它用来作为测试函数,将不同的参数传入 fac()和reverse()。通过这个函数可以判断前两个函数是否能正常工作。
现在编译这段代码。许多类UNIX系统都含有gcc编译器,在这些系统上可以使用下面的命令。
$ gcc Extest1.c -o Extest
$
要运行代码,可以执行下面的命令并获得输出。
$ Extest
4! == 24
8! == 40320
12! == 479001600
reversing 'abcdef', we get 'fedcba'
reversing 'madam', we get 'madam'
$
再次强调,必须尽可能先完善扩展程序的代码。把针对 Python 程序的调试与针对扩展库本身bug的调试混在一起是一件非常痛苦的事情。换句话说,将调试核心代码与调试Python程序分开。与Python接口的代码写得越完善,就越容易把它集成进Python并正确工作。
这里每个函数都接受一个参数,也只返回一个参数。这简单明了,因此集成进Python应该不难。注意,到目前为止,还没涉及任何与Python相关的内容。仅仅创建了一个标准的C或C++应用而已。
8.2.2 根据样板编写封装代码
完整地实现一个扩展都围绕“封装”相关的概念,读者应该熟悉这些概念,如组合类、修饰函数、类委托等。开发者需要精心设计扩展代码,无缝连接Python和相应的扩展实现语言。这种接口代码通常称为样板(boilerplate)代码,因为如果需要与 Python 解释器交互,会用到一些格式固定的代码。
样板代码主要含有四部分。
1.包含Python头文件。
2.为每一个模块函数添加形如PyObject*Module_func()的封装函数。
3.为每一个模块函数添加一个PyMethodDef ModuleMethods[]数组/表。
4.添加模块初始化函数void initModule()。
包含Python头文件
首先要做的是找到Python包含文件,并确保编译器可以访问这个文件的目录。在大多数类UNIX系统上,Python包含文件一般位于/usr/local/include/python2.x或/usr/include/python2.x中,其中2.x是Python的版本。如果通过编译安装的Python解释器,应该不会有问题,因为系统知道安装文件的位置。
将Python.h这个头文件包含在源码中,如下所示。
include "Python.h"
这部分很简单。下面需要添加样板软件中的其他部分。
为函数编写形如PyObject* Module_func()的封装函数
这一部分有点难度。对于每个需要在Python 环境中访问的函数,需要创建一个以static PyObject*标识,以模块名开头,紧接着是下划线和函数名本身的函数。
例如,若要让fac()函数可以在Python中导入,并将Extest作为最终的模块名称,需要创建一个名为Extest_fac()的封装函数。在用到这个函数的Python脚本中,可以使用import Extest和 Extest.fac()的形式在任意地方调用 fac()函数(或者先 from Extest import fac,然后直接调用fac())。
封装函数的任务是将Python中的值转成成C形式,接着调用相应的函数。当C函数执行完毕时,需要返回Python的环境中。封装函数需要将返回值转换成Pytho形式,并进行真正的返回,传回所有需要的值。
在 fac()的示例中,当客户程序调用 Extest.fac()时,会调用封装函数。这里会接受一个Python整数,将其转换成C整数,接着调用C函数fac(),获取返回结果,同样是一个整数。将这个返回值转换成Python整数,返回给调用者(记住,编写的封装函数就是def fac(n)声明的代理函数。当这个封装函数返回时,就相当于Python fac()函数执行完毕了)。
现在读者可能会问,怎样才能完成这种转换?答案是在从Python到C时,调用一系列的PyArg_Parse*()函数,从C返回Python时,调用Py_BuildValue()函数。
这些PyArg_Parse*()函数与C中的sscanf()函数类似。其接受一个字节流,然后根据一些格式字符串进行解析,将结果放入到相应指针所指的变量中。若解析成功就返回1;否则返回0。
Py_BuildValue()的工作方式类似sprintf(),接受一个格式字符串,并将所有参数按照格式字符串指定的格式转换为一个Python对象。
表8-1总结了这些函数。
表8-1 在Python和C/C++之间转换数据
在Python和C之间使用一系列的转换编码来转换数据对象。转换编码见表8-2。
表8-2 Python和C/C++之间的“转换编码”
(续表)
① Python 2和Python 3之间的格式编码基本相同。
② 与“O”类似,但不递增对象的引用计数。
这些转换编码用在格式字符串中,用于指出对应的值在两种语言中应该如何转换。注意,其转换类型不可用于Java中,Java中所有数据类型都是类。可以阅读Jython文档来了解Java类型和Python对象之间的对应关系。对于C#和VB.NET同样如此。
这里列出完整的Extest_fac()封装函数。
static PyObject *
Extest_fac(PyObject self, PyObject args) {
int res; // parse result
int num; // arg for fac()
PyObject* retval; // return value
res = PyArg_ParseTuple(args, "i", &num);
if (!res) { // TypeError
return NULL;
}
res = fac(num);
retval = (PyObject*)Py_BuildValue("i", res);
return retval;
}
封装函数中首先解析Python中传递进来的参数。这里应该是一个普通的整型变量,所以使用“i”这个转换编码来告知转换函数进行相应的操作。如果参数的值确实是一个整型变量,则将其存入num变量中。否则,PyArg_ParseTuple()会返回NULL,在这种情况下封装函数也会返回NULL。此时,它会生成TypeError异常来通知客户端用户,所需的参数应该是一个整型变量。
接着使用num作为参数调用fac()函数,将结果放在res中,这里重用了res变量。现在构建返回对象,即一个Python整数,依然通过“i”这个转换编码。Py_BuildValue()创建一个整型Python对象,并将其返回。这就是封装函数的所有内容。
实际上,当封装函数写多了后,就会试图简化代码来避免使用中间变量。尽量让代码保持可读性。这里将Extest_fac()函数精简成下面这个更短的版本,它只用了一个变量num。
static PyObject *
Extest_fac(PyObject self, PyObject args) {
int num;
if (!PyArg_ParseTuple(args, "i", &num))
return NULL;
return (PyObject*)Py_BuildValue("i", fac(num));
}
那 reverse()怎么实现?由于已经知道如何返回单个值,这里将对 reverse()的需求稍微修改下,返回两个值。将以元组的形式返回一对字符串,第一个元素是传递进来的原始字符串,第二个是新逆序的字符串。
为了更灵活地调用函数,这里将该函数命名为Extest.doppel(),来表示其行为与reverse()有所不同。将C代码封装进Extest_doppel()函数中,如下所示。
static PyObject *
Extest_doppel(PyObject self, PyObject args) {
char *orig_str;
if (!PyArg_ParseTuple(args, "s", &orig_str)) return NULL;
return (PyObject*)Py_BuildValue("ss", orig_str, \
reverse(strdup(orig_str)));
}
在Extest_fac()中,接受一个字符串值作为输入,将其存入orig_str中。注意,选择使用“s”这个转换编码。接着调用 strdup()来创建该字符串的副本。(因为需要返回原始字符串,同时需要一个字符串来逆序,所以最好的选择是直接复制原始字符串。)strdup()创建并返回一个副本,该副本立即传递给reverse()。这样就获得逆序后的字符串。
如你所见,Py_BuildValue()使用转换字符串“ss”将这两个字符串放到了一起。这里创建了含有原始字符串和逆序字符串的元组。都结束了吗?还没有。
这里遇到了 C 语言中一个危险的东西:内存泄露(分配了内存但没有释放)。内存泄露就相当于从图书馆借书,但是没有归还。在获取了某些资源后,当不再需要时,一定要释放这些资源。我们怎么能在代码中犯这样的错误呢(虽然看上去很无辜)?
当Py_BuildValue()将值组合到一个Python对象并返回时,它会创建传入数据的副本。在这里的例子中,创建了一对字符串。问题在于分配了第二个字符串的内存,但在结束时没有释放这段内存,导致了内存泄露。而实际想做的是构建返回值,接着释放在封装函数中分配的内存。为此,必须像下面这样修改代码。
static PyObject *
Extest_doppel(PyObject self, PyObject args) {
char *orig_str; // original string
char *dupe_str; // reversed string
PyObject* retval;
if (!PyArg_ParseTuple(args, "s", &orig_str)) return NULL;
retval = (PyObject*)Py_BuildValue("ss", orig_str, \
dupe_str=reverse(strdup(orig_str)));
free(dupe_str);
return retval;
}
这里引入了dupe_str变量来指向新分配的字符串并构建返回对象。接着使用free()来释放分配的内容,并最终返回给调用者。现在才算真正完成。
为模块编写PyMethodDef ModuleMethods[]数组
既然两个封装函数都已完成,下一步就需要在某个地方将函数列出来,以便让Python解释器知道如何导入并访问这些函数。这就是ModuleMethods[]数组的任务。
这个数组由多个子数组组成,每个子数组含有一个函数的相关信息,母数组以NULL数组结尾,表示在此结束。对Extest模块来说,创建下面这个ExtestMethods[]数组。
static PyMethodDef
ExtestMethods[] = {
{ "fac", Extest_fac, METH_VARARGS },
{ "doppel", Extest_doppel, METH_VARARGS },
{ NULL, NULL },
};
首先给出了在 Python 中访问所用到的名称,接着是对应的封装函数。常量METH_VARARGS 表示参数以元组的形式给定。如果使用 PyArg_ParseTupleAndKeywords()来处理包含关键字的参数,需要将这个标记与METH_KEYWORDS常量进行逻辑OR操作。最后,使用一对NULL来表示结束函数信息列表,还表示只含有两个函数。
添加模块初始化函数void init Module()
最后一部分是模块初始化函数。当解释器导入模块时会调用这段代码。这段代码中只调用了Py_InitModule()函数,其第一个参数是模块名称,第二个是ModuleMethods[]数组,这样解释器就可以访问模块函数。对于Extest模块,其initExtest()过程如下所示。
void initExtest() {
Py_InitModule("Extest", ExtestMethods);
}
现在已经完成了所有封装任务。将Extest1.c中原先的代码与所有这些代码合并到一个新文件Extest2.c中。至此,就完成了示例中的所有开发步骤。
另一种创建扩展的方式是先编写封装代码,使用存根(stub)函数、测试函数或假函数,在开发的过程中将其替换成具有完整功能的实现代码。通过这种方式,可以保证Python和C之间接口的正确性,并使用Python来测试相应的C代码。
8.2.3 编译
现在进入了编译阶段。为了构建新的Python封装扩展,需要将其与Python库一同编译。(从2.0版开始)扩展的编译步骤已经跨平台标准化了,简化了扩展编写者的工作。现在使用distutils包来构建、安装和发布模块、扩展和软件包。从Python 2.0开始,这种方式替换了老版本1.x中使用makefile构建扩展的方式。使用distutils,可以通过下面这些简单的步骤构建扩展。
1.创建setup.py。
2.运行setup.py来编译并链接代码。
3.在Python中导入模块。
4.测试函数。
创建setup.py
第一步就是创建setup.py文件。大部分编译工作由setup()函数完成。在该函数之前的所有代码都只是预备步骤。为了构建扩展模块,需要为每个扩展创建一个Extension实例。因为这里只有一个扩展,所以只需一个Extension实例。
Extension('Extest', sources=['Extest2.c'])
第一个参数是扩展的完整名称,以及该扩展中拥有的所有高阶包。该名称应该使用完整的点分割表示方式。由于这里是个独立的包,因此名称为“Extest”。sources参数是所有源码文件的列表。同样,只有一个文件Extest2.c。
现在就可以调用setup()。其接受一个命名参数来表示构建结果的名称,以及一个列表来表示需要构建的内容。由于这里是创建一个扩展,因此设置一个含有扩展模块的列表,传递给ext_modules。语法如下所示。
setup('Extest', ext_modules=[…])
由于这里只有一个模块,因此将扩展模块的实例化代码集成到setup()的调用中,在预备步骤中将模块名称设置为“常量”MOD。
MOD = 'Extest'
setup(name=MOD, ext_modules=[
Extension(MOD, sources=['Extest2.c'])])
setup()中含有许多其他选项,这里就不一一列举了。读者可以在官方的Python文档中找到关于创建setup.py和调用setup()的更多信息,在本章末尾可以找到这些链接。示例8-2显示了示例扩展中用到的完整脚本。
示例8-2 构建脚本(setup.py)
这段脚本将扩展编译到build/lib.*子目录中。
运行setup.py来编译并链接代码
既然有了setup.py文件,就运行python setup.py build命令构建扩展。这里在Mac上完成构建(根据操作系统和Python版本的不同,对应的输出与下面的内容会有些差别)。
$ python setup.py build
running build
running build_ext
building 'Extest' extension
creating build
creating build/temp.macosx-10.x-fat-2.x
gcc -fno-strict-aliasing -Wno-long-double -no-cpp-
precomp-mno-fused-madd -fno-common -dynamic -DNDEBUG –g
-I/usr/include -I/usr/local/include -I/sw/include -I/
usr/local/include/python2.x -c Extest2.c -o build/temp.macosx-10.x-
fat2.x/Extest2.o
creating build/lib.macosx-10.x-fat-2.x
gcc -g -bundle -undefined dynamic_lookup -L/usr/lib -L/
usr/local/lib -L/sw/lib -I/usr/include -I/usr/local/
include -I/sw/include build/temp.macosx-10.x-fat-2.x/Extest2.o -o
build/lib.macosx-10.x-fat-2.x/Extest.so
8.2.4 导入并测试
最后一步是回到Python中使用扩展包,就像这个扩展就是用纯Python编写的那样。
在Python中导入模块
扩展模块会创建在build/lib.*目录下,即运行setup.py脚本的位置。要么切换到这个目录中,要么用下面的方式将其安装到Python中。
$ python setup.py install
如果安装该扩展,会得到下面的输出。
running install
running build
running build_ext
running install_lib
copying build/lib.macosx-10.x-fat-2.x/Extest.so ->
/usr/local/lib/python2.x/site-packages
现在可以在解释器中测试模块了。
>>> import Extest
>>> Extest.fac(5)
120
>>> Extest.fac(9)
362880
>>> Extest.doppel('abcdefgh')
('abcdefgh', 'hgfedcba')
>>> Extest.doppel("Madam, I'm Adam.")
("Madam, I'm Adam.", ".madA m'I ,madaM")
添加测试函数
需要完成的最后一件事是添加测试函数。实际上,我们已经有测试函数了,就是那个main()函数。但要小心,在扩展代码中含有 main()函数有潜在的风险,因为系统中应该只有一个main()函数。将main()的名称改成test()并对其封装可以消除这个风险,添加Extest_test()并更新ExtestMethods数组,如下所示。
static PyObject *
Extest_test(PyObject self, PyObject args) {
test();
return (PyObject*)Py_BuildValue("");
}
static PyMethodDef
ExtestMethods[] = {
{ "fac", Extest_fac, METH_VARARGS },
{ "doppel", Extest_doppel, METH_VARARGS },
{ "test", Extest_test, METH_VARARGS },
{ NULL, NULL },
};
Extest_test()模块函数仅仅运行test()并返回一个空字符串,在Python中是一个None值返回给调用者。
现在可以在Python中进行相同的测试。
>>> Extest.test()
4! == 24
8! == 40320
12! == 479001600
reversing 'abcdef', we get 'fedcba'
reversing 'madam', we get 'madam'
>>>
示例8-3中列出了Extest2.c的最终版本,上述输出都是用这个版本来完成的。
示例8-3 C函数库的Python封装版本(Extest2.c)
在这个示例中,仅在同一个文件中将原始的C代码与Python相关的封装代码进行了隔离。这样方便阅读,在这个短小的例子中也没有什么问题。但在实际应用中,源码文件会越写越大,可以将其分割到不同的源码文件中,使用如ExtestWrappers.c这样好记的名字。
8.2.5 引用计数
也许读者还记得Python使用引用计数来追踪对象,并释放不再引用的对象。这是Python垃圾回收机制的一部分。当创建扩展时,必须额外注意如何处理Python对象,必须留心是否需要修改此类对象的引用计数。
一个对象有两种类型的引用,一种是拥有引用(owned reference),对该对象的引用计数递增1表示拥有该对象的所有权。当从零创建一个Python对象时,就一定会含有一个拥有引用。
当使用完一个 Python 对象后,必须对所有权进行处理,要么递减其引用计数,通过传递它转移其所有权,要么将该对象存储到其他容器。如果没有处理引用计数,则会导致内存泄漏。
对象还有一个借用引用(borrowered reference)。相对来说,这种方式的责任就小一些。一般用于传递对象的引用,但不对数据进行任何处理。只要在其引用计数递减至零后不继续使用这个引用,就无须担心其引用计数。可以通过递增对象的引用计数来将借用引用转成拥有引用。
Python提供了一对C宏来改变Python对象的引用计数。如表8-3所示。
表8-3 用于执行Python对象引用计数的宏
在上面的Extest_test()函数中,在构建PyObject对象时使用空字符串来返回None。但可以通过拥有一个None对象来完成这个任务。即递增一个PyNone的引用计数并显式返回这个对象,如下所示。
static PyObject *
Extest_test(PyObject self, PyObject args) {
test();
Py_INCREF(Py_None);
return PyNone;
}
Py_INCREF()和 Py_DECREF()还有一个先检测对象是否为 NULL 的版本,分别为Py_XINCREF()和Py_XDECREF()。
这里强烈建议读者阅读相关Python文档中关于扩展和嵌入Python里面所有关于引用计数的细节(详见附录C中的参考文献部分)。
8.2.6 线程和全局解释器锁
扩展的编写者必须要注意,他们的代码可能在多线程Python环境中执行。4.3.1节介绍了Python虚拟机(Python Virtual Machine,PVM)和全局解释器锁(Global Interpreter Lock,GIL),描述了在PVM中,任意时间只有一个线程在执行,GIL就负责阻止其他线程的执行。除此之外,还指出了调用外部函数的代码,如扩展代码,将会锁住GIL,直至外部函数返回。
但也提到了一种折衷方法,即让扩展开发者释放GIL。例如,在执行系统调用前就可以实现。这是通过将代码和线程隔离实现的,这些线程使用了另外的两个 C 宏:Py_BEGIN_ALLOW_THREADS和Py_END_ALLOW_THREADS,保证了运行和非运行时的安全性。用这些宏围起来的代码块会允许其他线程在其执行时同步执行。
与引用计数宏相同,这里也建议读者阅读Python文档中关于扩展和嵌入Python的内容,以及Python/C API参考手册。
8.3 相关主题
本章最后一节将介绍其他用来编写扩展的工具,如SWIG、Pyrex、Cython、psyco和PyPy。最后简要讨论一个相关主题,即嵌入Python,以此来结束本章。
8.3.1 SWIG
这款称为简化的封装和接口生成器(Simplified Wrapper and Interface Generator,SWIG)的外部工具,由David Beazley编写,他同时也是Python Essential Reference的作者。这款工具可以将注释过的C/C++头文件生成可以用于封装Python、Tcl和Perl的封装代码。使用SWIG可以从本章前面介绍的样板代码中解放出来。只须关注如何用C/C++解决实际问题。所要做的就是按照SWIG的格式创建相应的文件,SWIG会为用户完成剩下的工作。在下面的链接中可以找到关于SWIG的更多信息。
http://en.wikipedia.org/wiki/SWIG
8.3.2 Pyrex
通过Python C API或SWIG创建C/C++扩展的一个缺点就是必须要编写C/C++代码。这种方式虽然可以使用 C/C++的强大功能,但更麻烦的是,也会遇到其中的陷阱。Pyrex 提供了一种新的方式,既可以利用扩展的优势,也不必牵扯到C/C++这些令人头疼的内容。Pyrex是一种新的语言,专门用于编写Python扩展。它是C和Python的混合体,但更接近于Python。实际上,在Pyrex官网上,描述是“Pyrex是含有C数据类型的Python”。所要做的就是以Pyrex语言编写代码,并运行Pyrex编译器编译代码,Pyrex会创建C文件,用来编译成普通的扩展。通过Pyrex可以永远脱离C语言。可以在Pyrex官网获得Pyrex。
http://cosc.canterbury.ac.nz/~greg/python/Pyrex
http://en.wikipedia.org/wiki/Pyrex_(programming_language)
8.3.3 Cython
Cython起始于2007年一个Pyrex的分支, Cython的第一个版本是0.9.6,与Pyrex 0.9.6同时出现。与Pyrex开发团队的谨慎相比,Cython开发者在Cpython的开发过着中更加敏捷和激进。这导致了向Cython添加的补丁、改进和扩展比Pyrex要多得多。但这两个项目都是活跃的项目。可以通过下面的链接来阅读关于Cython及其与Pyrex区别的更多信息。
http://wiki.cython.org/DifferencesFromPyrex
8.3.4 Psyco
Pyrex和Cython都可以不用编写纯C代码之外。但需要学习新的语法(还有新的语言)。最终,Pyrex/Cython 的代码都将转成 C 的形式。开发者编写扩展或使用类似 SWIG 或Pyrex/Cython的工具来提升效率。但如果只用Python编写代码就能获得这样的性能提升呢?Pysco的理念与前面的方式有很大不同。除了编写C代码之外,为什么不直接让已有的Python代码运行得更快呢?Psyco类似一个即时(JIT)编译器,所以不需要改变Python源码,只需导入Psyco模块,让其在运行时优化代码。
Pysco还可以分析Python代码,找出其中瓶颈所在。甚至可以启用日志来查看Pysco在优化时做了哪些工作。唯一的限制是其只支持32位的Intel 386架构(Linux、Mac OS X、Windows、BSD),其中运行Python 2.2.2-2.6.x版本,而不是3.x版。在撰写本书时,对2.7版本的支持尚未完成。更多信息参见下面的链接 [6]。
http://en.wikipedia.org/wiki/Psyco
8.3.5 PyPy
PyPy是Psyco的继承项目。其有一个非常宏伟的目标,即为开发解释型语言创建一个独立于平台或目标执行环境的通用开发环境。PyPy自身从零起步,使用Python 编写创建一个Python 解释器。大部分人仍然都是这么认为的,但实际上,这个特定的解释器是整个 PyPy生态系统的一部分。
这些工具集含有许多实用的东西,允许语言设计者仅仅关注解释型语言的解析和语义分析。而翻译到本地架构所有困难的部分时,如内存管理、字节码转换、垃圾回收、数值类型的内部表示、原始数据结构、原生架构等,工具集都会为设计者处理周全。
这是通过用含有限制、静态类型的Python(称为Rpython)来实现新语言的。前面提到, Python是第一种实现的目标语言,所以用RPython编写一个Python解释器与PyPy的字面意思很接近。但通过RPython可以实现任何语言,而不仅仅是Python。
这个工具链会将RPython代码转成某种底层显示,如C、Java字节码或通用中间语言(CIL),即根据通用语言结构(Common Language Infrastructure, CLI)标准编写的语言字节码。换句话说,解释型语言的开发只需考虑语言设计,而很少关心其实现和目标架构。更多信息见下面的链接。
http://en.wikipedia.org/wiki/PyPy
8.3.6 嵌入Python
嵌入是Python的另一项特性。其与扩展相反,不是将C代码封装进Python中,而是在C应用中封装Python解释器。这样可以为一个庞大、单一、要求严格、专有,并(或)针对关键任务的应用利用Python解释器的强大功能。一旦在C环境中拥有了Python解释器,就进入一个全新的领域。
Python提供了许多官方文档供扩展开发者来查阅相关信息。
http://docs.python.org/extending/embedding中是其中一些与本章相关的Python文档的链接。
扩展和嵌入
Python/C API
发布Python模块
http://docs.python.org/distutils
8.4 练习
8-1 扩展Python。Python扩展有哪些优点?
8-2 扩展Python。使用扩展有哪些缺点和危险?
8-3 编写Python扩展。获得一个C/C++编译器,并熟悉C/C++编程。创建一个简单的工具函数,并配置成一个扩展。在C/C++和Python中执行并验证扩展。
8-4 从Python移植到C。选取前几章中的一些练习,将其作为扩展模块移植到C/C++中。
8-5 封装C代码。选取很久之前编写但想转成Python的一段C/C++代码。将其作为一个扩展模块。
8-6 编写扩展Core Python Programming或Core Python language Fundamentals。在面向对象编程章节的一个练习中,在一个类中创建一个dollarize()函数,用来将浮点数格式化为金融格式的字符串。创建一个扩展来封装dollarize()函数,并集成一个回归测试函数(如test())到模块中。选做题:除了创建C 扩展之外,还在Pyrex 和Cython中重写dollarize()。
8-7 扩展与嵌入。扩展和嵌入有哪些区别?
8-8 不编写扩展。将练习8-3、8-4或8-5中编写的C/C++代码在Pyrex或Cython中用伪Python代码重写。描述使用Pyrex/Cython编写代码与将C/C++代码集成进C扩展中的经验。
[1].通过网址http://www.educause.edu/EDUCAUSE+Review/EDUCAUSEReviewMagazineVolume39/Musing sontheInternetPart2/157899回到2004年。
[2].投递寄件人类似在信封外壳上写的寄件人和收件人地址,邮局根据这个地址进行投递。而“真实”的收件人和寄件人是指在信封里面的信件上写出来的这封信是由谁寄出的,只有收件人会看到。SMTP一般只根据投递寄件人/收件人发送邮件,而不查看“真实”的收件人和寄件人。参考资料:[1] https://utcc.utoronto.ca/usg/technotes/smtp-intro.html;[2] http://stackoverflow.com/questions/1750194/why-does-email-need-an-envelope-and-what-does-the-envelope-mean;[3] https://www.pobox.com/helpspot/index.php?pg =kb.page&id=260。——译者注
[3].这段话和本书第2版相同,但是在第3版中实际上已经使用了MySQL和SQLite两个数据库,db参数并不再无用,且该脚本中参数名为dsn。——译者注
[4].如本节所述,SQLObject并不支持Python 3.x版本。——译者注
[5].原文将关系数据库的列和MongoDB的集合放到一起,会产生列和集合等价的误解。实际上关系数据库的列对应的应该是MongoDB的字段(field),而MongoDB的集合对应的是关系数据库的表。——译者注
[6].Pysco已于2012年3月12日终止。——译者注
