Python优化:用C来扩展
原作:kortis.to
翻译:nasi
恩,你要用你最喜欢的语言来开发你的项目,当然这个语言就是Python咯 :) 但是同时,你还要面对计算量非常大的问题。但是,用Python来作大规模的计算通常并不太适合,因为它是一门解释性的语言。听起来是不是满熟悉的?
优化,优化…
当然,优化你的python代码是一个方法。这个在这里并不深入讨论,这里有优化python代码不错的资料。另外还有一个方法,就是使用更加有效率的算法。这个是让你的程序变快的一个主要方法。
但是,如果你已经优化了你的代码了呢?而且你也使用了你所知的运行最快的算法,但是程序依然像乌龟一样慢呢?恩,还有一个方法(除了升级你的电脑):用C(或者C++)来扩展你的程序。用Python舒舒服服的来写其他的部分,而最耗CPU的部分就用C来写。
案例学习: 计算素数
比如说,你有一个问题,需要计算出前n个素数。(为什么?恩,这就取决于你了 :) )然后,你用python来写,写出来大概这个样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | import sys def isprime1(input): if input < 1: return 0 n = input - 1 while n > 1: if input%n == 0: return 0 n = n - 1 return 1 def main(): i = 0 l = 0 max = int(sys.argv[1]) while 1: if isprime1(i): l = l + 1 i = i + 1 if l == max: break print max, "primes calculated" if __name__ == "__main__": main() |
这个isprime1函数用来计算一个数是不是素数。我猜这应该不是“最优算法”,不过这不重要。得益于Python的高可读性,这个代码不需要任何注释了,对吧?:)
这个程序运行起来很漂亮,但是,你应该注意到,随着素数数量的增加,程序会开始变得非常慢。通过我们的观察,同样的C程序大概要快上30倍!哇,用Python就要等上30倍吗?这是C的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include <stdio.h> #include <stdlib.h> int isprime1(int input) { int n; if (input < 1) { return 0; } n = input - 1; while (n > 1){ if (input%n == 0) return 0; n--; } return 1; } int main(int argc, char *argv[]){ int i=0, l=0; int max; max = atoi(argv[1]); while (1) { if (isprime1(i)==1) { l++; } i++; if (l==max){ break; } } printf("%i primes calculated\n",max); } |
看上去和Python版本的同样漂亮。
解决方案
那么,现在该怎么办?正如我一开始说的那样,我们把素数检测的部分用C来写。直觉上来说,它应该管用,但是不会非常管用。再猜猜…
比较
让我们来看一下我做的计时… 这有个图来说明计算前n个素数所用的时间。
正如你看到的,用纯Python来写的代码比起C的版本很快就变的越来越慢了。但是,令人惊讶的是,用Python+C的版本竟然几乎和纯C版本的程序一样快。太好了。
下面再来深入看看纯C版本和Python+C版本的程序之间的对比。
随着n的增大,C版本的程序和Python+C版本的程序之间的差距越来越小。真是没有比这再好的消息了! :)
扩展
那么,什么是扩展?怎么去做扩展?从根本上说,扩展无非就是用C来写一个Python的module罢了。更加深入的细节和过程的文档都在Python文档中有叙述,当然,这里我将给你一个小示例来给你开开胃。
第一步
确定你要用C来写什么。在我们这个示例中,很明显素数检测就是我们要用C来写、用来提速的东东。
我们已经写过这个函数了,现在要做的就是要让它和Python“兼容”而已。我们把新的源代码命名为“prmodule.c”。(我在这里展示的示例除了计算素数以外,其实是对Python文档的模仿,所以想要更多的信息请参考Python文档)
在prmodule.c中的isprime函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include <Python.h> static PyObject *pr_isprime(PyObject *self, PyObject *args){ int n, input; if (!PyArg_ParseTuple(args, "i", &input)) return NULL; if (input < 1) { return Py_BuildValue("i", 0); } n = input - 1; while (n > 1){ if (input%n == 0) return Py_BuildValue("i", 0); n--; } return Py_BuildValue("i", 1); } |
这个函数需要返回一个PyObject指针,同时还需要将PyObject指针作为参数。这个就是我们从python的调用中获得的真实参数。PyArg_ParseTuple()用来解析从python传递过来的参数。我们从参数出解析出一个整数,通过这个整数来告诉我们需要检查多少个素数。当返回一个值的时候,我们通过python对象传递回去。
简单直接吧,你一定这么想。但是,prmodule.c还远远没结束呢。我们需要告知我们的module中有哪些方法是可用的。最后,还要为module做一个初始化。
所以,我们添加下面这些到prmodule.c的源文件中:
22 23 24 25 26 27 28 29 | static PyMethodDef PrMethods[] = { {"isprime", pr_isprime, METH_VARARGS, "Check if prime."}, {NULL, NULL, 0, NULL} }; void initpr(void){ (void) Py_InitModule("pr", PrMethods); } |
第二步
接下来,我们需要编译一下我们的module。这里有一个非常好的distutils包正是干这个的。可能你已经从很多python的库中看到了这些名叫setup.py的文件。这就是我们要做的。(我们没有在任何地方实际的安装任何东西,仅仅是编译而已)
让我们写一下setup.py:
1 2 3 4 | from distutils.core import setup, Extension module = Extension('pr', sources = ['prmodule.c']) setup(name = 'Pr test', version = '1.0', ext_modules = [module]) |
然后运行“python setup.py build”,这样就会编译这个module。就是这样!编译的结果将会在build/lib.linux-i686-2.1(或者类似的)目录中,命名是“pr.so”。你可以把它复制到你的源代码所在的目录,然后和其他module一样的使用它,比如:
comet:~/prog/python/prime$ python Python 2.1.3 (#1, Apr 20 2002, 10:14:34) [GCC 2.95.4 20011002 (Debian prerelease)] on linux2 Type "copyright", "credits" or "license" for more information. >>> import pr >>> pr.isprime(17) 1 >>>
















事实上,现在可以借助SWIG来自动为已有的C语言代码生成封装代码以方便编译生成PYTHON模块。还可以使用BOOST中PYTHON库用C++来编写PYTHON的扩展模块(文档中说只支持PYTHON2.2)。PyQT项目中也有一个类似的工具,只是名字忘了。
bluesky said this on 8月 12th, 2007 at 5:23 下午