Python量化交易基础讲堂-Python扩展C加快执行速度

933 阅读7分钟

Python实战-构建基于股票的量化交易系统》小册子,虽然主要侧重于 Python 实战讲解,但在内容设计上提供了前置基础章节帮助读者快速掌握基础工具的使用,因此小册适合的人群仅具备Python最基础编程经验即可。

同时我们会持续更新一些关于Python和量化相关的基础文章,帮助大家夯实基础。接下来我们介绍下Python中扩展C语言加快执行速度的实现方法。

当我们提到一门编程语言的效率时,通常包含了开发效率和运行效率这两层意思。Python作为一门高级语言,它功能强大,易于掌握,能够快速的开发软件,“life is short,we use python!”,想必这些优点是毋庸置疑的,但是作为一门解释性语言,执行速度的局限性导致在处理某些高频任务时存在不足。

由于Python本身由C语言实现的,开发性能要求较高的程序模块可以通过扩展运行效率更高的C语言来弥补自身的弱点。另外有些算法已经有开源的C库,那么也没必要用Python重写一份,只需要通过Python进行C库的调用即可。

接下来通过实例介绍如何在Python 程序中整合既有的C语言模块,从而充分发挥Python 语言和 C 语言各自的优势。

Python实现测试函数

使用Python编写一个递归函数和循环函数,应用Python的计时库timeit测试函数执行10000次所需要的时间分别为57ms和41ms。

实现代码如下:

from timeit import timeit  

def factorial(n):
    if n<2:return 1
return factorial(n-1)*n 
def rooporial(n):
    if n<2:return 1
    ans = 1
    for i in range(1,n+1):
        ans *=i
    return ans 

if __name__ == '__main__': 
print "factorial",factorial(20),timeit('factorial(20)','from __main__ import factorial',number=10000) #timeit(‘函数名’,‘运行环境’,number=运行次数)
print "rooporial",rooporial(20),timeit('rooporial(20)','from __main__ import rooporial',number=10000)

打印返回:

factorial 2432902008176640000 0.0578598976135
factorial 2432902008176640000 0.0410023010987

当然递归方法使程序的结构简洁,但由于它逐层深入调用的机制使得执行效率不如循环,以下的测试可以发现每一次递归是新一次的函数调用,会产生新的局部变量,增加了执行时间。但是即使使用For循环实现也需要41ms时间,接下来我们尝试更快的实现方式。

测试代码如下:


def up_add_down(n):
    print("level %d: n location %p\n",n,id(n))
    if n<=4:up_add_down(n+1)
    print("level %d: n location %p\n",n,id(n))
    return 

打印返回:

('level %d: n location %p\n', 0, 144136380)
('level %d: n location %p\n', 1, 144136368)
('level %d: n location %p\n', 2, 144136356)
('level %d: n location %p\n', 3, 144136344)
('level %d: n location %p\n', 4, 144136332)
('level %d: n location %p\n', 5, 144136320)
('level %d: n location %p\n', 5, 144136320)
('level %d: n location %p\n', 4, 144136332)
('level %d: n location %p\n', 3, 144136344)
('level %d: n location %p\n', 2, 144136356)
('level %d: n location %p\n', 1, 144136368)
('level %d: n location %p\n', 0, 144136380)

Python源生调用实现

Python在设计之初就考虑到通过足够抽象的机制让C和C++之类的编译型的语言导入到Python脚本代码中,在Python的官方网站上也找到了扩展和嵌入Python解释器对应的方法。这里介绍下如何将C编写的函数扩展至Python解释器中。

将C编写的递归函数存为wrapper.c,作为Python的扩展库。同时需要对C函数增加一个型如PyObject* Module_func()的封装接口,该接口用于Python解释器的交互。将封装接口加入至型如PyMethodDef ModuleMethods[]的数组中,Python解释器能够从数组中导入并调用到封装接口。最后是实现对扩展库的初始化函数,调用Py_InitModule()函数,把扩展库和ModuleMethods[]数组的名字传递进去,以便于解释器能正确的调用库中的函数。

wrapper.c实现代码如下:

#include <Python.h>

unsigned long long factorial(int n)
{
    if(n<2)return 1;
    return factorial(n-1)*n;
}

PyObject* wrap_fact(PyObject* self,PyObject* args)
{
    int n;
    unsigned long long  result;
    
    if(!PyArg_ParseTuple(args,"i:fact",&n))return NULL;//i 整形
    result = factorial(n);
    return Py_BuildValue("L",result);//L longlong型
}

static PyMethodDef wrapperMethods[] = 
{
    {"fact",wrap_fact,METH_VARARGS,"Caculate N!"},//METH_NOARGS无需参数/METH_VARARGS需要参数;
    {NULL,NULL},
};
int initwrapper()
{
    PyObject* m;
    m = Py_InitModule("wrapper",wrapperMethods);//参数:扩展库名称/库所包含的方法
    return 0;
}
  • 安装python-dev包含Python.h头文件。安装命令:sudo apt-get python-dev

  • 在linux环境下wrapper.c编译成动态链接库wrapper.so

  • 编译命令:gcc wrapper.c -fPIC -shared -o wrapper.so -I/usr/include/python2.7

  • Python文件中import wrapper导入动态链接库,在import语句导入库时会执行初始化函数

Python文件中wrapper.fact()方式对C函数调用时,封装函数wrap_fact()先会被调用,封装函数接收到一个Python整形对象,PyArg_ParseTuple将Python整形对象转为C整形参数,然后调用C的factorial()函数并将C整数参数传入,经过运算后得到一个C长整形的返回值,Py_BuildValue把C长整形返回值转为Python的长整形对象作为最终整个函数调用的结果。

timeit测试函数wrapper.fact(20)执行10000次所的时间只需要5.9ms。

factorial_rc 2432902008176640000 0.00598216056824

Python Ctypes模块调用实现

Python内建ctypes库使用了各个平台动态加载动态链接库的方法,并在Python源生代码基础上通过类型映射方式将Python与二进制动态链接库相关联,实现Python与C语言的混合编程,可以很方便地调用C语言动态链接库中的函数。(ctypes源码路径:/Modules/_ctypes/_ctypes.c、/Modules/_ctypes/callproc.c)

将C编写的递归函数存为a.c,不需要对C函数经过Python接口封装

#include <stdio.h>   
#include <stdlib.h>   
unsigned long long factorial(int n)
{
    if(n<2)return 1;
    return factorial(n-1)*n;
}
(2)在linux环境下a.c编译成动态链接库a.so
编译命令:gcc a.c -fPIC -shared -o a.so 
Python文件中调用动态链接库a.so,在Windows平台下,最终调用的是Windows API中LoadLibrary函数和GetProcAddress函数,在Linux和Mac OS X平台下,最终调用的是Posix标准中的dlopen和dlsym函数。ctypes库内部完成PyObject* 和C types之间的类型映射,使用时只需设置调用参数和返回值的转换类型即可。
from ctypes import cdll
from ctypes import * 
libb = cdll.LoadLibrary('./a.so') 
def factorial_c(n):
    return libb.factorial(n)

if __name__ == '__main__':
libb.factorial.restype=c_ulonglong#返回类型
	libb.factorial.argtype=c_int#传入类型
print "factorial_c",factorial_c(20),timeit('factorial_c(20)','from __main__ import factorial_c',number=10000)

(4)timeit测试函数libb.factorial(20)执行10000次所的时间需要8.5ms。
factorial_c 2432902008176640000 0.00857210159302

总结

factorial、factorial_c、factorial_rc分别为Python脚本、ctypes 库和Python源生代码扩展方式来实现函数的执行时间。通过执行时间对比可以发现调用C函数来扩展Python功能可以大大提高执行速度,而Python自带的ctypes库由于在源生代码上进行封装,执行时间会高于源生代码扩展方式,但ctypes库使用方便,特别适合应用在第三方封装代码,提供动态链接库和调用文档的场合。

关于完整代码可以加入小册交流群获取。更多的量化交易内容欢迎大家订阅小册阅读!!