?? 引言/动机
?? 扩展 Python
?? 创建应用程序代码
?? 用样板包装你的代码
?? 编译
?? 导入并测试
?? 引用计数
?? 线程和 GIL
?? 相关话题
22.1 介绍/动机
什么是扩展
一般来说,所有能被整合或导入到其它python 脚本的代码,都可以被称为扩展。您可以用纯
Python 来写扩展,也可以用C 和C++之类的编译型的语言来写扩展(或者也可以用Java 给Jython 写
扩展,也可以用C#或Visual Basic.NET 给IronPython 写扩展)。
Python 的一大特点就是,扩展和解释器之间的交互方式与普通的Python 模块完全一样。Python
在设计之初就考虑到要让模块的导入机制足够抽象。抽象到让使用模块的代码无法了解到模块的具
体实现细节。除非那个程序员在磁盘中搜索这个模块文件,否则,他/她就连这个模块到底是用Python
写的,还是用某种编译语言写的都分辨不了。
核心笔记:在不同平台上创建扩展
我们要注意的是,如果你曾自己编译过Python 解释器,那么,在这样的环境中,扩展一般都是
可以使用的。自己手动编译扩展,和获取扩展的二进制文件是有一些不一样的。虽然自己编译比简
单的下载安装复杂一些,但由此得来的好处就是,你可以自由选择你想使用的Python 的版本。
虽然本章中的例子都是在Unix 系统中开发的(一般的unix 中,都自带编译器)。但只要你能使
用C/C++(或Java)的编译器并且C/C++(或Java)中有Python 的开发环境。那唯一的区别只是怎样来
编译而已。无论在哪一个平台上,真正起作用的代码都是一样的。如果你在Win32 平台上进行开发,
你需要有Visual C++开发环境。Python 的发布包中自带了7.1 版本的项目文件。当然,你也可以使
用老版本的VC。
想了解更多的关于如何在Win32 上开发扩展的信息,你可以访问如下网页:
http://docs.python.org/ext/building-on-windows.html
警告:就算是相同的架构的两台电脑之间最好也不要互相共享二进制文件。最好是在各自的电
脑上编译Python 和扩展。因为,有时就算是编译器或是CPU 之间的些许差异,也会导致代码不能正
常工作。
为什么要扩展Python?
纵观软件工程的历史,编程语言都不具备可扩展性,你只能使用已有的功能,而不能为语言增加新功能。
?? 添加/额外的(非 Python)功能。扩展Python 的一个原因就是出于对一些新功能的需要,
而Python 语言的核心部分并没有提供这些功能。这时,通过纯Python 代码或者编译扩展都
可以做到。但是有些情况,比如创建新的数据类型或者将Python 嵌入到其它已经存在的应
用程序中,则必须得编译。
?? 性能瓶颈的效率提升。众所周知,由于解释型的语言是在运行时动态的翻译解释代码,这导
致其运行速度比编译型的语言慢。一般说来,把所有代码都放到扩展中,可以提升软件的整
体性能。但有时,由于时间与精力有限,这样做并不划算。通常,先做一个简单的代码性能
测试,看看瓶颈在哪里,然后把瓶颈部分在扩展中实现会是一个比较简单有效的做法。效果
立竿见影不说,而且还不用花费太多的时间与精力。
?? 保持专有源代码私密。创建扩展的另一个很重要的原因是脚本语言都有一个共同的缺陷,那
就是所有的脚本语言执行的都是源代码,这样一来源代码的保密性便无从谈起了。把一部分
代码从Python 转到编译语言就可以保持专有源代码私密。因为,你只要发布二进制文件就
可以了。编译后的文件相对来说,更不容易被反向工程出来。因此,代码能实现保密。尤其
是涉及到特殊的算法,加密方法以及软件安全的时候,这样做就显得非常至关重要了。
另一种对代码保密的方法是只发布预编译后的.pyc 文件。这是介于发布源代码(.py 文件)和把
代码移植到扩展这两种方法之间的一种较好的折中的方法。
22.2 创建Python 扩展
1. 创建应用程序代码
2. 利用样板来包装代码
3. 编译与测试
创建您的应用程序代码
首先,我们要建立的是一个“库”,要记住,我们要建立的是将在Python 内运行的一个模块。
所以在设计你所需要的函数与对象的时候要注意到,你的C 代码要能够很好的与Python 的代码进行
双向的交互和数据共享。
然后,写一些测试代码来保障你的代码的正确性。你可以在C 代码中放一个main()函数,使得
你的代码可以被编译并链接成一个可执行文件(而不是一个动态库),当你运行这个可执行文件时,
程序可以对你的软件库进行回归测试。这种是一种很符合Python 风格的做法。
我们要再强调一次,你应该尽可能的完善你的代码。因为,在把代码集成到Python 中后再来调
试你的核心代码,查找潜在的bug 是件很痛苦的事情。也就是说,调试核心代码与调试集成这两件
事应该分开来做。要知道,与Python 的接口代码写得越完善,集成的正确性就越容易保证。
1 //Pure C Version of Library (Extest1.c) 2 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <string.h> 6 7 int fac(int n) { 8 if (n < 2) return (1); // 0! == 1! == 1 9 return (n)*fac(n-1); // n! == n*(n-1)! 10 } 11 12 char *reverse(char *s){ 13 register char t, //temp 14 *p = s, //fwd 15 *q = (s + (strlen(s)-1)); //bwd 16 17 while (p < q){ // if p < q 18 // swap & mv ptrs 19 t = *p; 20 *p++ = *q; 21 *q-- = t; 22 } 23 return s; 24 } 25 26 int main(){ 27 char s[BUFSIZ]; 28 printf("4! == %d\n", fac(4)); 29 printf("8! == %d\n", fac(8)); 30 printf("12! == %d\n", fac(12)); 31 strcpy(s, "abcdef"); 32 printf("reversing ‘abcdef‘, we get ‘%s‘\n", 33 reverse(s)); 34 strcpy(s, "madam"); 35 printf("reversing ‘madam‘, we get ‘%s‘\n", 36 reverse(s)); 37 return 0; 38 }
Extest1.c
用样板来包装你的代码
整个扩展的实现都是围绕着“包装”这个概念进行的。
你的设计要尽可能让你的实现语言与Python 无缝结合。
接口的代码被称为“样板”代码,它是你的代码与Python 解释器之间进行交互所必不可少的一部分。
我们的样板主要分为4 步:
1. 包含Python 的头文件。
2. 为每个模块的每一个函数增加一个型如PyObject* Module_func()的包装函数。
3. 为每个模块增加一个型如PyMethodDef ModuleMethods[]的数组。
4. 增加模块初始化函数void initModule()
包含Python 头文件
#include "Python.h"
为每个模块的每一个函数增加一个型如PyObject* Module_func()的包装函数
你需要为所有想被Python 环境访问的函数都增加一个静态的函数,函数的返回值类型为PyObject*,函数名前面要加上模块名和一个下划线(_)。
包装函数的用处就是先把Python 的值传递给C,然后调用我们想要调用的相关函数。
当这个函数完成要返回Python 的时候,把函数的计算结果转换成Python 的对象,然后返回给Python。
在从Python 到C 的转换就用 PyArg_Parse*系列函数。
在从C 转到Python 的时候,就用Py_BuildValue()函数。
PyArg_Parse 系列函数
PyArg_Parse 系列函数的用法跟C 的sscanf 函数很像,都接受一个字符串流,并根据一个指定的格式字符串进行解析,把结果放入到相应的指针所指的变量中去。
它们的返回值为1 表示解析成功,返回值为0 表示失败。
Py_BuildValue函数
Py_BuildValue 的用法跟sprintf 很像,把所有的参数按格式字符串所指定的格式转换成一个 Python 的对象。
这些转换代码出现在格式字符串当中,用于指定各个值的数据类型,以便于在两种语言之间做转换。
注:由于Java 的所有数据类型都是类,所以Java 的转换类型不一样。Python 对象在Java 中所对应的数据类型请参考Jython 的相关文档。C#也有同样的问题。
1 //Pure C Version of Library (Extest1.c) 2 3 #include <stdio.h> 4 #include <stdlib.h> 5 #include <string.h> 6 #include "..\include\python.h" 7 8 int fac(int n) { 9 if (n < 2) return (1); // 0! == 1! == 1 10 return (n)*fac(n-1); // n! == n*(n-1)! 11 } 12 13 static PyObject *Extest_fac(PyObject *self, PyObject *args) { 14 int res; // parse result 15 int num; // arg for fac() 16 PyObject* retval; // return value 17 18 res = PyArg_ParseTuple(args, "i", &num); // format code ‘i‘ 19 if(!res) { // TypeError 20 return NULL; 21 } 22 res = fac(num); 23 retval = (PyObject*)Py_BuildValue("i", res); // format code ‘i‘ 24 return retval; 25 } 26 27 static PyObject *Extest_fac2(PyObject *self, PyObject *args) { 28 int num; 29 if (!PyArg_ParseTuple(args, "i", &num)) 30 return NULL; 31 return (PyObject*)Py_BuildValue("i", fac(num)); 32 } 33 34 char *reverse(char *s){ 35 register char t, //temp 36 *p = s, //fwd 37 *q = (s + (strlen(s)-1)); //bwd 38 39 while (p < q){ // if p < q 40 // swap & mv ptrs 41 t = *p; 42 *p++ = *q; 43 *q-- = t; 44 } 45 return s; 46 } 47 48 static PyObject *Extest_reverse(PyObject *self, PyObject *args) { 49 char *origin; 50 if(!PyArg_ParseTuple(args, "s", &origin)) 51 return NULL; 52 return (PyObject*)Py_BuildValue("s", reverse(origin)); 53 } 54 55 static PyObject *Extest_doppel(PyObject *self, PyObject *args) { 56 char *orig_str; 57 char *dupe_str; 58 PyObject *retval; 59 if(!PyArg_ParseTuple(args, "s", &orig_str)) 60 return NULL; 61 62 dupe_str = strdup(orig_str); 63 retval = (PyObject*)Py_BuildValue("ss", // format code "ss" mapping to tuple 64 dupe_str, 65 reverse(orig_str)); 66 free(dupe_str); 67 return retval; 68 } 69 70 int test(){ 71 char s[BUFSIZ]; 72 printf("4! == %d\n", fac(4)); 73 printf("8! == %d\n", fac(8)); 74 printf("12! == %d\n", fac(12)); 75 strcpy(s, "abcdef"); 76 printf("reversing ‘abcdef‘, we get ‘%s‘\n", 77 reverse(s)); 78 strcpy(s, "madam"); 79 printf("reversing ‘madam‘, we get ‘%s‘\n", 80 reverse(s)); 81 return 0; 82 } 83 84 static PyObject *Extest_test(PyObject *self, PyObject *args) { 85 test(); 86 return (PyObject*)Py_BuildValue(""); 87 } 88 89 static PyMethodDef ExtestMethods[] = { 90 { "fac", Extest_fac, METH_VARARGS }, 91 { "reverse", Extest_reverse, METH_VARARGS }, 92 { "doppel", Extest_doppel, METH_VARARGS }, 93 { "test", Extest_test, METH_VARARGS }, 94 { NULL, NULL}, 95 }; 96 97 void initExtest() { 98 Py_InitModule("Extest", ExtestMethods); 99 }
Extest2.c
为每个模块增加一个型如PyMethodDef ModuleMethods[]的数组
这个数组由多个数组组成。
每一个数组都包含了函数在Python 中的名字,相应的包装函数的名字以及一个METH_VARARGS 常量。
METH_VARARGS 常量表示参数以tuple 形式传入。
如果我们要使用PyArg_ParseTupleAndKeywords() 函数来分析命名参数的话, 我们还需要让这个标志常量与METH_KEYWORDS 常量进行逻辑与运算常量。
最后,用两个NULL 来结束我们的函数信息列表。
增加模块初始化函数void initModule()
所有工作的最后一部分就是模块的初始化函数。
这部分代码在模块被导入的时候被解释器调用。
在这段代码中,我们需要调用Py_InitModule()函数,并把模块名和ModuleMethods[]数组的名字传递进去,以便于解释器能正确的调用我们模块中的函数。
创建扩展的另一种方法是先写包装代码,使用桩函数,测试函数或哑函数。在开发过程中慢慢
的把这些函数用有实际功能的函数替换。这样,你可以确保Python 和C 之间的接口函数是正确的,
并用它们来测试你的C 代码。
编译
distutils 包被用来编译,安装和分发这些模块,扩展和包。
使用distutils 包的时候我们可以方便的按以下步骤来做:
1. 创建 setup.py
2. 通过运行setup.py 来编译和连接您的代码
3. 从Python 中导入您的模块
4. 测试功能
引用计数
Python 使用引用计数作为跟踪一个对象是否不再被使用,所占内存是否应该被回收的手段,它是垃圾回收机制的一部分。
当创建扩展时,你必需对如何操作Python 对象要格外的小心,你时时刻刻都要注意是否要改变某个对象的引用计数。
一个对象可能有两类引用分别是拥有引用和借引用。
Py_INCREF()和Py_DECREF()两个函数也有一个先检查对象是否为空的版本,分别为Py_XINCREF() 和Py_XDECREF()。
线程和全局解释锁(GIL)
Python 虚拟机(PVM)和全局解释锁(GIL)
在PVM 中,任何时候, 同时只会有一个线程被运行。其它线程会被GIL 停下来。而且,我们指出调用扩展代码等外部函数时,代码会被GIL 锁住,直到函数返回为止。
通过将您的代码和线程隔离实现的, 这些线程使用了另外的两个C 宏 Py_BEGIN_ALLOW_THREADS 和Py_END_ALLOW_THREADS 保证了运行和非运行时的安全性。
由这些宏包裹的代码将会允许其他线程的运行。
22.3 相关话题
SWIG
有一个外部工具叫SWIG,是Simplified Wrapper and Interface Generator 的缩写。其作者为
David Beazley,同时也是Python Essential Referenc 一书的作者。这个工具可以根据特别注释过
的C/C++头文件生成能给Python,Tcl 和Perl 使用的包装代码。使用SWIG 可以省去你写前面所说
的样板代码的时间。你只要关心怎么用C/C++解决你的实际问题就好了。你所要做的就是按SWIG 的
格式编写文件,其余的就都由SWIG 来完成。你可以通过下面的网址找到关于SWIG 的更多信息。
http://swig.org
Pyrex
创建C/C++扩展的一个很明显的坏处是你必须要写C/C++代码。你能利用它们的优点,但更重要
的是,你也会碰到它们的缺点。Pyrex 可以让你只取扩展的优点,而完全没有后顾之忧。它是一种更
偏向Python 的C 语言和Python 语言的混合语言。事实上,Pyrex 的官方网站上就说“Pyrex 是具有
C 数据类型的Python“。你只要用Pyrex 的语法写代码,然后运行Pyrex 编译器去编译源代码。Pyrex
会生成相应的C 代码,这些代码可以被编译成普通的扩展。你可以在它的官方网站下载到Pyrex:
http://cosc.canterbury.ac.nz/~greg/python/Pyrex
Psyco
Pyrex 免去了我们再去写纯C 代码的麻烦。不过,你要去学会它的那一套与众不同的语法。最后,
你的Pyrex 代码还是会被转成C 的代码。无论你用C/C++,C/C++加上SWIG,还是Pyrex,都是因为
你想要加快你的程序的速度。如果你可以在不改动你的Python 代码的同时,又能获得速度的提升,
那该多好啊。
Psyco 的理念与其它的方法截然不同。与其改成C 的代码,为何不让你已有的Python 代码
运行的更快一些呢?
Psyco 是一个just-in-time(JIT)编译器,它能在运行时自动把字节码转为本地代码运行。所以,
你只要(在运行时)导入Psyco 模块,然后告诉它要开始优化代码就可以了。而不用修改自己的代
码。
Psyco 也可以检查你代码各个部分的运行时间,以找出瓶颈所在。你甚至可以打开日志功能,来
查看Psyco 在优化你的代码的时候,都做了些什么。你可以访问以下网站获取更多的信息:
http://psyco.sf.net
嵌入
嵌入是Python 的另一功能。与把C 代码包装到Python 中的扩展相对的,嵌入是把Python 解释
器包装到C 的程序中。这样做可以给大型的,单一的,要求严格的,私有的并且(或者)极其重要
的应用程序内嵌Python 解释器的能力。一旦内嵌了Python,世界完全不一样了。
Python 提供了很多官方文档供写扩展的人参考。
下面是一些与本章相关的Python 文档:
扩展与嵌入
http://docs.python.org/ext
Python/C API
http://docs.python.org/api
分发Python 模块
http://docs.python.org/dist