这段时间一直在用Python,虽然Python语言容易入手,但期间纠结的地方也挺多,整理一下,以备后用!
出于项目组的需要,我用python主要是做C扩展方面的工作,扩展方面我们主要使用静态扩展:
首先要有纯C的代码,然后在纯C代码的基础上进行一系列的包装就行,用C写的函数部分不用做任何更改,我们只需要了解怎样包装;
首先,我们要建立的是一个“库”,要记住,我们要建立的是将在Python内运行的一个模块。所以在设计你所需要的函数与对象的时候要注意到,你的C代码要能够很好地与Python的代码进行双向的交互和数据共享。
然后,写一些测试代码来保障你的代码的正确性。你可以在C代码中放一个main()函数,使得你的代码可以被编译并链接成一个可执行文件(而不是一个动态库),当你运行这个可执行文件时,程序可以对你的软件库进行回归测试。这是一种很符合Python风格的做法。
在下面的例子中,我们就将采用这种做法。测试用例分别针对我们想要导出到Python世界的两个函数。一个是递归求阶乘的函数fac()。另一个reverse()函数则实现了一个简单的字符串反转算法,其主要目的是修改传入的字符串,使其内容完全反转,但不需要用申请内存后反着复制的方法。由于涉及到指针的使用,我们务必要在设计和调试时小心谨慎,以防把问题带入Python。
例22.1中所列出的Extest1.c是我们的第一个版本。
代码中,包含了两个函数fac()和reverse()。分别实现了我们刚刚所说的两个功能。fac()接受一个整型参数并递归计算结果,在退出最后一层调用后最终返回到调用代码中。
最后一段代码是必要的main()函数。我们在这里面写测试代码,传不同的参数给fac()和reverse()。有了这个函数,我们就可以了解我们的代码是否能得到正确的结果。
现在,我们就可以编译这段代码了。在大部分有gcc编译器的Unix系统中,我们都可以用以下指令进行编译:
$ gcc Extest1.c -o Extest
$
我们可以输入以下命令来运行我们的程序,并得到如下输出:
$ Extest
4! == 24
8! == 40320
12! == 479001600
reversing 'abcdef', we get 'fedcba'
reversing 'madam', we get 'madam'
$
例22.1 纯C版本库(Extestl.c)
下面列出了我们想要包装并在Python解释器中使用的C函数的代码,main()是测试函数。
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4
5 int fac(int n)
6 {
7 if (n < 2) return(1); /* 0! == 1! == 1 */
8 return (n)*fac(n-1); /* n! == n*(n-1)! */
9 }
10
11 char *reverse(char *s)
12 {
13 register char t, /* 中间变量t */
14 char *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 {
28 char s[BUFSIZ];
29 printf("4! == %d\n", fac(4));
30 printf("8! == %d\n", fac(8));
31 printf("12! == %d\n", fac(12));
32 strcpy(s, "abcdef");
33 printf("reversing 'abcdef', we get '%s'\n", \
34 reverse(s));
35 strcpy(s, "madam");
36 printf("reversing 'madam', we get '%s'\n", \
37 reverse(s));
38 return 0;
39 }
我们要再强调一次,你应该尽可能地完善你的代码。因为,在把代码集成到Python中后再来调试你的核心代码,查找潜在的bug是件很痛苦的事情。也就是说,调试核心代码与调试集成这两件事应该分开来做。要知道,与Python的接口代码写得越完善,集成的正确性就越容易保证。
我们的两个函数都只接受一个参数,并返回一个值。这是很标准的情况,与Python集成的时候应该不会有什么问题。注意,到现在为止,我们所做的都还与Python没什么关系。我们只是简单地创建了一个C/C++的应用程序而已。
22.2.2 用样板来包装你的代码
整个扩展的实现都是围绕着13.15.1节所说的“包装”这个概念进行的。你的设计要尽可能让你的实现语言与Python无缝结合。接口的代码被称为“样板”代码,它是你的代码与Python解释器之间进行交互所必不可少的一部分。
我们的样板主要分为4步。
1.包含Python的头文件。
2.为每个模块的每一个函数增加一个形如PyObject* Module_func()的包装函数。
3.为每个模块增加一个形如PyMethodDef ModuleMethods[]的数组。
4.增加模块初始化函数void initModule()。
1.包含Python头文件
首先,你要找到Python的头文件在哪,并且确保你的编译器有权限访问它们。在大多数类Unix的系统里,它们都会在/usr/local/include/python2.x或/usr/include/python2.x目录中。其中,“2.x”是你所使用的Python的版本号。如果你曾编译并安装过Python解释器,那应该不会碰到什么问题,因为这时,系统一般都会知道你的文件安装在哪。像下面这样在你的代码里加入一行:
#include "Python.h"
这部分比较简单。接下来再看看怎么在样板中加入其他的部分。
2.为每个模块的每一个函数增加一个型如PyObject* Module_func()的包装函数
这一部分最需要技巧。你需要为所有想被Python环境访问的函数都增加一个静态的函数,函数的返回值类型为PyObject*,函数名前面要加上模块名和一个下划线( _ )。
比方说,我们希望在Python中,能够导入(import)我们的fac()函数,其所在的模块名为Extest,那么我们就要创建一个包装函数叫Extest_fac()。在使用这个函数的Python脚本中,使用方法是先“import Extest”然后调用“Extest.fac()”(或者先“from Extest import fac”,然后直接调用“fac()”)
包装函数的用处就是先把Python的值传递给C,然后调用我们想要调用的相关函数。当这个函数完成要返回Python的时候,把函数的计算结果转换成Python的对象,然后返回给Python。
对于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的对象。
下面是完整的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异常,通知客户我们期望传入一个整型变量。
然后,我们会调用fac()函数,其参数为num,把返回结果放在res变量中。最后,通过调用Py_BuildValue()函数,格式字符串为“i”,把结果转为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()函数的区别。把代码包装到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()函数把这个字符串复制一份(由于我们要同时返回原始字符串和反转后的字符串,所以我们需要复制一份)。把新复制的字符串传给reverse函数,我们就得到了反转后的字符串。
如你所见,我们用“ss”格式字符串让Py_BuildValue()函数生成了一个含有两个字符串的元组,分别放了原始字符串和反转后的字符串。这样就完成所有的工作了吗?很不幸,还没有。
我们碰到了C语言的一个陷阱:内存泄漏。即内存被申请了,但没有被释放。就像去图书馆借了书,但是没有还一样。无论何时,你都应该释放所有你申请的,不再需要的内存。看,我们写的代码犯了多大的罪过啊(虽然看上去好像很无辜的样子)!
Py_BuildValue()函数生成要返回的Python对象的时候,会把转入的数据复制一份。上例中,那两个字符串就会被复制出来。问题就在于,我们申请了用于存放第二个字符串的内存,但是,在退出的时候没有释放它。于是,这片内存就泄漏了。正确的做法是:先生成要返回的对象,然后释放在包装函数中申请的内存。我们必须要这样修改我们的代码:
static PyObject *
Extest_doppel(PyObject *self, PyObject *args) {
char *orig_str; // 原始字符串
char *dupe_str; // 反转后的字符串
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常量。其中,METH_VARARGS常量表示参数以元组形式传入。如果我们要使用PyArg_ParseTupleAndKeywords()函数来分析命名参数的话,我们还需要让这个标志常量与METH_KEYWORDS常量进行逻辑与运算常量。最后,用两个NULL来结束我们的函数信息列表。
3.增加模块初始化函数void initModule()
所有工作的最后一部分就是模块的初始化函数。这部分代码在模块导入的时候被解释器调用。在这段代码中,我们需要调用Py_InitModule()函数,并把模块名和ModuleMethods[]数组的名字传递进去,以便解释器能正确地调用我们模块中的函数。对Extest模块来说,initExtest()函数应该是这个样子的:
void initExtest() {
Py_InitModule("Extest", ExtestMethods);
}
这样,所有的包装都已经完成了。我们把以上代码与之前的Extest1.c合并到一个新文件——Extest2.c中。到此为止,我们的开发阶段就已经结束了。
创建扩展的另一种方法是先写包装代码,使用桩函数、测试函数或哑函数。在开发过程中慢慢地把这些函数用有实际功能的函数替换。这样,你可以确保Python和C之间的接口函数是正确的,并用它们来测试你的C代码。
22.2.3 编译
现在,我们已经到了编译阶段。为了让你的新Python扩展能被创建,你需要把它们与Python库放在一起编译。现在已经有了一套跨30多个平台的规范,它极大地方便了编写扩展的人。distutils包被用来编译、安装和分发这些模块、扩展和包。这个模块在Python2.0的时候就已经出现了,并用于代替1.x版本时的用Makefile来编译扩展的方法。使用distutils包的时候我们可以方便地按以下步骤来做:
1.创建 setup.py;
2.通过运行setup.py来编译和连接您的代码;
3.从Python中导入您的模块;
4.测试功能。
1.创建setup.py
下一步就是要创建一个setup.py文件。编译最主要的工作由setup()函数来完成。在这个函数调用之前的所有代码,都是一些预备动作。为了能编译扩展,你要为每一个扩展创建一个Extension实例,在这里,我们只有一个扩展,所以只要创建一个Extension实例:
Extension('Extest', sources=['Extest2.c'])
第一个参数是(完整的)扩展的名字,如果模块是包的一部分的话,还要加上用“.”分隔的完整的包的名字。我们这里的扩展是独立的,所以名字只要写“Extest”就好了。sources参数是所有源代码的文件列表。同样,我们也只有一个文件Extest2.c。
现在,我们可以调用setup()了。Setup()需要两个参数:一个名字参数表示要编译哪个东西,一个列表列出要编译的对象。由于我们要编译的是一个扩展,我们把ext_modules参数的值设为扩展模块的列表。语法如下:
setup('Extest', ext_modules=[...])
例22.2
这个脚本会把我们的扩展编译到build/lib.*子目录中。
1 #!/usr/bin/env python
2
3 from distutils.core import setup, Extension
4
5 MOD = 'Extest'
6 setup(name=MOD, ext_modules=[
7 Extension(MOD, sources=['Extest2.c'])])
由于我们只有一个模块,我们把我们扩展模块对象的实例化操作放到了setup()的调用代码中。模块的名字我们就传预先定义的“常量”MOD:
MOD = 'Extest'
setup(name=MOD, ext_modules=[
Extension(MOD,sources=['Extest2.c'])])
setup()函数还有很多选项可以设置。限于篇幅,不能完全罗列。读者可以在本章最后所列的官方文档中找到setup.py和setup()函数相关的信息。例22.2给出了我们例子所要用的完整的脚本代码。
2.通过运行setup.py来编译和连接代码
现在,我们已经有了setup.py文件。运行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-fat-2.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
22.2.4 导入和测试
1.从Python中导入您的模块
你的扩展会被创建在你运行setup.py脚本所在目录下的build/lib.*目录中。你可以切换到那个目录中来测试你的模块,或者也可以用以下命令把它安装到你的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")
2.测试功能
我们想要做的最后一件事就是加上一个测试函数。事实上,我们已经写好一个了,就是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中调用同样的test()函数了:
>>> Extest.test()
4! == 24
8! == 40320
12! == 479001600
reversing 'abcdef', we get 'fedcba'
reversing 'madam', we get 'madam'
>>>
在例22.3中,我们给出了Extest2.c的最终版本。这个版本会输出我们刚才所看到的结果。
在本例中,我们把我们的C代码和Python相关的代码分开放,一段在上面,一段在下面。
这样可以让代码更具可读性。对于小程序来说,没有任何问题。但在实际应用中,源代码会越写越大。一部分人就会考虑把他们的包装函数放在另一个源文件中。起个诸如ExtestWrappers.c之类好记的名字。
22.2.5 引用计数
也许你还记得,Python使用引用计数作为跟踪一个对象是否不再被使用,所占内存是否应该被回收的手段。它是垃圾回收机制的一部分。当创建扩展时,你必需对如何操作Python对象格外小心。你时时刻刻都要注意是否要改变某个对象的引用计数。
一个对象可能有两类引用。一种是拥有引用,你要对这个对象的引用计数加1,以表示你也拥有这个对象的所有权。如果这个Python对象是你自己创建的,那么这时你肯定拥有这个对象的所有权。
当你不再需要一个Python对象时,你必须要交出你的所有权,要么把引用计数减1,要么把所有权交给别人,要么就把这个对象存到其他的容器中(元组、列表等)。没有交出所有权就会导致内存泄漏。
你也可以拥有对象的借引用。相对来说,这种方式的责任就小一些。除非是别人在外面把对象传递给你。否则,不要用任何方式修改对象里的数据。你也不用时刻考虑对象引用计数的问题,只要你不会在对象的引用计数减为0之后再去使用这个对象。你也可以把借引用对象用的数量加1,从而真正地引用这个对象。
例22.3 C库的Python包装版本(Extest2.c)
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4
5 int fac(int n)
6 {
7 if (n < 2) return(1);
8 return (n)*fac(n-1);
9 }
10
11 char *reverse(char *s)
12 {
13 register char t,
14 char *p = s,
15 *q = (s + (strlen(s) - 1));
16
17 while (s && (p < q))
18 {
19 t = *p;
20 *p++ = *q;
21 *q-- = t;
22 }
23 return s;
24 }
25
26 int test()
27 {
28 char s[BUFSIZ];
29 printf("4! == %d\n", fac(4));
30 printf("8! == %d\n", fac(8));
31 printf("12! == %d\n", fac(12));
32 strcpy(s, "abcdef");
33 printf("reversing 'abcdef', we get '%s'\n", \
34 reverse(s));
35 strcpy(s, "madam");
36 printf("reversing 'madam', we get '%s'\n", \
37 reverse(s));
38 return 0;
39 }
40
41 #include "Python.h"
42
43 static PyObject *
44 Extest_fac(PyObject *self, PyObject *args)
45 {
46 int num;
47 if (!PyArg_ParseTuple(args, "i", &num))
48 return NULL;
49 return (PyObject*)Py_BuildValue("i", fac(num));}
50 }
51
52 static PyObject *
53 Extest_doppel(PyObject *self, PyObject *args)
54 {
55 char *orig_str;
56 char *dupe_str;
57 PyObject* retval;
58
59 if (!PyArg_ParseTuple(args, "s", &orig_str))
60 return NULL;
61 retval = (PyObject*)Py_BuildValue("ss", orig_str, \
62 dupe_str=reverse(strdup(orig_str)));
63 free(dupe_str);
64 return retval;
65 }
66
67 static PyObject *
68 Extest_test(PyObject *self, PyObject *args)
69 {
70 test();
71 return (PyObject*)Py_BuildValue("");
72 }
73
74 static PyMethodDef
75 ExtestMethods[] =
76 {
77 { "fac", Extest_fac, METH_VARARGS },
78 { "doppel", Extest_doppel, METH_VARARGS },
79 { "test", Extest_test, METH_VARARGS },
80 { NULL, NULL },
81 };
82
83 void initExtest()
84 {
85 Py_InitModule("Extest", ExtestMethods);
86 }