Python的C扩展,学习整理

这段时间一直在用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()函数在返回一样)。

那么怎样才能完成这样的转换呢?答案是,在从PythonC的转换就用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中。到此为止,我们的开发阶段就已经结束了。

创建扩展的另一种方法是先写包装代码,使用桩函数、测试函数或哑函数。在开发过程中慢慢地把这些函数用有实际功能的函数替换。这样,你可以确保PythonC之间的接口函数是正确的,并用它们来测试你的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   }

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值