作为python程序员,应该能够正视python的优点与缺点。众所周之,python的运行速度是很慢的,特别是大数据量的运算时,python会慢得让人难以忍受。对于这种情况,“专业”的解决方案是用上numpy或者opencl。不过有时候为了一点小功能用上这种重型的解决方案很不划算,或者有时候想要实现的操作在numpy里面没有,需要我们自己用c语言来编写。总之,我们使用python与c++的混合编程能够加快程序热点的运算速度。
首先要提醒大家注意的是,在考虑联合编程之前一定要找到程序运行的热点。简单一点地,使用标准库的profile或者cprofile模块找到最消耗cpu的位置,如果这个位置只简单的消耗io时间,通常换成c++程序的意义也不会很大,此时做联合编程可能是事倍功半,起不到多大的效果。
还有些情况,python程序员们想要使用操作系统或者外部模块提供的函数。这些模块一般是为c/c++程序员提供的。这时候也是python与c++联合编程的用武之地。
python语言可以说是最好的胶水语言。仅就与c++联合编程这个问题来讲,依使用难度与功能来排列,python社区提供了以下几种解决方案:
1.使用标准库ctypes直接调用c/c++编写的动态链接库。这是最简单易用的方案。c/c++程序员使用自己的丰富的经验,把预定的功能实现为动态链接库。而python程序员只要知道这些动态链接库函数的名称、参数类型与返回值类型就能简单地调用它。当你传入参数时,ctypes模块会自动地把python的对象成为c/c++所对应的参数类型。比如以下调用windows的api:
#定义参数类型与函数名称
from ctypes.wintypes import uint, dword
getlastinputinfo = ctypes.windll.user32.getlastinputinfo
class lastinputinfo(ctypes.structure):
_fields_ = [(“cbsize”, uint),
(“dwtime”, dword)]
#开始调用dll导出的函数
def getlastinputtime_nt():
info = lastinputinfo()
info.cbsize = ctypes.sizeof(info)
info.dwtime = 0
if not getlastinputinfo(ctypes.byref(info)):
raise windowserror(“”)
return info.dwtime
在这里展示了如何构造windows的api所需要的结构体,如何填充结构体并分析返回值。
ctypes还能将python函数提供给c/c++代码作为回调函数。
与其它解决方案相比。ctypes不需要程序员熟悉c/c++语言,不需要安装一个c/c++的编译器,它通过操作系统的接口直接操作c/c++代码。而且ctypes是标准库的一部分,只要安装了python就可以直接使用。这几个原因使得它深受python程序员的喜爱。
而它的劣势呢。首先,ctypes不能简单调用c++程序,因为c++在编译的时候使用了name mangling这个技术来实现函数的重载。c++会自动地为类的成员函数加上类名前缀。所以,c++程序员需要以c语言的调用约定来提供接口,没有类,没有重载函数,没有模板,没有c++异常。不能直接调用现有的c++代码可能是这个方案最大的缺点。
另外,对于list, set之类的数据类型,ctypes不能识别并自动地在python与c/c++数据类型之间转换。c/c++部分不能识别python数据类型,这时候只能用python语言来编写转换代码。如果数据量较大,或者调用很频繁,转换代码反而会浪费很多的资源。这或许是ctypes的另一个劣势之一了。
2.如果你使用的是jython或者ironpython的话,它们也提供了类似于ctypes之类的模块,能够直接访问java或者.net语言编写的模块。其优势与劣势大致与ctypes相似。因为其使用范围有限,这里不再详述。
3.使用cython语言,一种类似于python语言的一种新型语言编写预定功能的代码,然后将这些代码转换成为c语言编译成为python语言可以直接调用的二进制模块。cython语言是融合python语言与c语言的一种新型语言。它本身能够理解python语言的语法,然后在其基础上增加了某些c语言的语法,以便更精细地控制数据类型与指针。基本兼容python语法是这个解决方案最大的特点。很多时候,python程序员只要在旧的代码中简单地声明一下代码中所使用的参数、变量的类型,就能把立即为旧的python程序提速。
cython提供了一个名为pyximporter的工具,能够在安装了c/c++编译器的计算机上面为简单的cython程序直接生成相应的python模块。这使得cython的使用与普通的python程序一样简单。比如下面这段代码,直接保存为myhello.pyx即可被调用。
#myhello.pyx
def sayhellotentimes():
cdef int i #只要简单地为变量标识类型即可加速循环。
for i in range(0, 10):
print(“hello, world!”)
$ python
>>> import pyximport; pyximport.install()
>>> import myhello
>>> myhello.sayhellotentimes()
由此可见,cython非常容易使用。而且不仅能够处理c语言的模块,还能处理c++的模块——虽然没有直接支持虚函数之类的完整c++特性。因为它不直接使用c/c++语法,而是另外设计比c/c++更简洁优雅的新型语法,因此,对于不熟悉c/c++的程序员来说有很大的吸引力。相比ctypes来说,因为参数类型转换更加智能与高效,所以通常能够提升更多的效率。
劣势呢,所谓用python程序员所熟练的语法来编写高速的运算代码,乍一听相当地有吸引力。但是如果想要更深入地控制内存与数据结构时,程序员可能会发现,现在他不得不熟练地掌握c/c++语言,然后用cython的语法写出来。以程序员们懒惰的性格,这反而是件难以忍受的事件。这或许是cython本身并不大流行的主要原因吧。
4.使用boost.python。有意思的是,与ctypes/cython形成鲜明的对比,boost.python倾向于让c++程序员拥有更熟悉的编程环境。它让c++程序员使用他所熟悉的c++语法直接控制python的数据结构,调用python的解释器。它没有像cython那样发明新的语法,而是直接使用c++的语法,编写供python使用的接口。与cython同样的道理,它的效率优胜于ctypes。
与cython/swig/sip等方案相比,程序员只需要学习c/c++与python两种语言。另外,与本文提到的几种解决方案相比,它非常适合在主要由c++编写的程序中控制python代码。不仅功能更强大、效率还更高。如此神奇的解决方案会有什么劣势呢?某些人可能不同意吧,老鱼一听说它依赖于boost就蔫了,感觉编译与学习庞大又奇怪的boost非常浪费生命。
5.使用swig或者sip,通过编写一个接口文件,使用类似于c/c++语法——声明函数、类型的信息,然后使用特殊的工具为c/c++的代码生成python的接口代码。这些接口代码能够在python与c/c++之间的数据结构转换。最终编译这些接口代码,成为python的二进制模块。swig与sip的接口文件与c/c++的头文件非常相似。
这两种工具差不多,因为。本质上,他们都与cython类似,都使用了中间语言来生成转换代码。但swig/sip能够在他们的接口文件中嵌入c/c++,能够让程序员仔细地调节数据类型的转换过程。在使用上,它比cython的层次更低,更接近于python本身提供的api。
swig能够为多种脚本语言生成转换代码。而sip则专门针对python与c++。此外,sip本身是作为pyqt的专门工具来开发的,因此它能够理解qt的signal/slot。从应用项目上来看,swig似乎会更广泛一点。而sip,目前所见的项目基本都与pyqt相关。据说swig对于c++的支持不好,不知道有没有人来说一下呢。相比之下,sip对于c++的支持非常完善,诸如虚函数、protected member function、模版、析构函数、异常等特性都得到良好的支持。而且sip支持python的gil,还拥有一个使用python编写的编译系统。可能会更方便一点。
然而这种方案毕竟要学习一种新的语言,所以从表面上来看不如cython和boost.python讨喜。当程序员想要仔细地调节类型转换代码的时候,需要学习swig/sip的内部机制,被限定使用特殊的变量名。这使得这种方案的学习曲线相对较高。
6.直接使用python的api,可以称之为最终解决方案。cython, swig, sip的接口文件转换后所生成的c/c++代码实际上都使用python的api。与其它方案相比,这种方案相当地繁复,必须为每次函数调用编写数据转换代码,还要操心python对象的引用计数。我觉得这种方案一无是处,这时就不再多讲了。其它的工具pybindgen不知道什么情况。有兴趣的话可以看看。
好了。题外话一句吧,我一直觉得ctypes与xmlrpc并列python语言的两大神器,最能体现python的生产效率。
希望本文在大家选择一种技术路线时能提供一点点帮助。