文章目录
听过很多人说python速度慢,难以用于正式项目。本文介绍运用Cython编程,令Python代码的运行速度能提升数倍至数百倍。
1. 什么是 Cython?
Cython是Python编程语言的一个超集,Cython 允许Python 与 C/C++风格代码混合编程,编译后可以象普通 python模块那样导入使用。Python解释器运行时遇到Cython代码则以静态类型方式调用。这样,在性能上就得到很大提升。
目前,有许多知名的Python库,如NumPy和Pandas已经使用Cython来提高性能。在实现项目开发过程中,用cProfile 分析项目性能慢时,通常会发现,瓶颈通常只是由个别函数或API造成的。将1个Python函数改写为Cython风格,通常只需要做很少改动就可以完成。
写Cython代码并不复杂。通常不需要学习C++指针、模板语法、STL标准库等知识,这样一来, Cython使用到的C++语法就很简单了,与添加了类型提示的Python语法很相似,只是形式上稍有不同而已。所以Python中应用Cython还是挺容易的,非常值得Python程序员尝试。
如果性能慢是由于API接口, I/O等造成,这个问题在所有编程语言都存在, 请用异步,多线程方式来优化。
Cython与 types使用上的区别:
- 如果需要对Python代码加速,或者在python中使用C/C++源码,应使用 Cython;
- 如果只是需要在python项目中使用已经编译好的C/C++库文件,使用 types 来封装更简单一些。 当然Cython也支持,但二者运行速度上没有区别,这种场景 types使用上更简单。
下面我将演示如何用cython来改写1个python函数,并编译为二进制文件,测试比较前后性能变化。
2. 用 Cython 编写1个函数
1) 安装 cython
建议使用Python3.9 以上版本
pip3 install cython
2) 先编写1个纯python函数
我们编写1个函数,求圆周率近似值,数学公式如下
下面代码使用纯python实现此计算,默认n = 10000000, 并导入cProfile 模块来计算各函数消耗时间。
# 文件名: pi.py
import cProfile
def recip_square(i):
return 1. / i ** 2
def approx_pi(n=10000000):
val = 0.
for k in range(1, n + 1):
val += recip_square(k)
pi = (6 * val) ** .5
print("Approximate value of pi is: ", pi)
return pi
if __name__ == '__main__':
# 计算PI, 并统计耗时
cProfile.run('approx_pi(10000000)')
3)使用cython重写该函数
cython 编程就是在python中使用C类型来申明变量,要先申明再使用。允许导入 C++原码。
cython代码:
- 头文件(.pxd),与 C语言的.h 头文件作用相同,主要包含变量与函数声明
- 实现文件(.pyx/.py)
.pxd头文件不是必须的,只要有.pyx就可以了。
.pyx文件中通常包含以下内容:
- cimport 引入语句:用于从其它cython模块、pxd头文件中引入Cython函数、类\、变量等,也可以引入c++ STL类型,或者外部c/c++ 模块。当然也可以用 import 引入python模块。
- Cython静态类型声明变量: cdef 开头,使用c类型声明变量, 允许使用指针类型(无须手工释放)
- Cython函数, 以 cdef 开头的函数
- 允许包含 python 代码,python所有语法在.pyx中都支持
- 可以使用Cython自定义类
- 控制语句、表达式,与python相同。
用 Cython 语法将上一节示例做简单的改写如下
# 文件名 cpi.pyx
# cython: profile=True
cimport cython
@cython.profile(False)
cdef inline double recip_square(long long i):
return 1.0 / (i * i)
def approx_pi(int n=10000000):
cdef double val = 0.
cdef int k
for k in range(1, n + 1):
val += recip_square(k)
pi = (6 * val) ** .5
print("Approximate value of pi is: ", pi)
return pi
代码分析
下面这段代码, 用cython语法声明val, k 为静态类型
cdef double val = 0.
cdef int k
其中 double, int 是 C 语言浮点数、整数类型, cdef 表示使用静态编译
cdef inline double recip_square(long long i)
这条语句说明,这是1个 cython 函数。
定义与使用 cython 函数时注意事项:
- cython函数以
cdef
开, 其函数风格与C函数基本相同, 须用C类型申明函数参数与返回值。 - for 循环的循环变量对运行速度很较大影响,必须在for循环之前有Cython语法声明循环变量,速度至少能提高几十倍。
- cdef 定义的cython函数,不能被外部python直接调用,.pyx文件用Python语法写1个调用Cython函数的包装器,供外部python调用。 本例中,
approx_pi()
相当于recip_square()
包装器, 外部调用的是approx_pi()
.
def approx_pi(int n=10000000):
approx_pi
函数名前面无 cdef ,表示这个函数是python函数,但其中包含cython语法定义的静态变量,在运行时会按python方式运行,解释器执行到 cdef double val = 0.
语句时,会从编译后的pyd动态链接库中调用C语句。
for k in range(1, n + 1):
val += recip_square(k)
这个代码块, 因为循环变量与调用函数都是Cython语法定义的,所以它的速度非常快。 函数的返回值 pi 变量是1个python变量。 但只调用了2次,因此对总体速度影响非常有限。
4) 编译 .pyx 文件
Cython 使用 Python 最通用的 setuptools 构建工具进行编译,但必须提前安装好C++编译器,linux上安装gcc, Windows 系统安装Visual Studio 或者minGW均可。
第1步,按setuptools 要求,编写setup.py构建脚本,setup.py构建的详细配置不赘述了,仅说明与cython相关的配置
# setup.py
from setuptools import setup
# 导入Cython 构建相关模块
from Cython.Build import cythonize
setup(
ext_modules=cythonize("cpi.pyx"),
)
其中 ext_modules=cythonize("cpi.pyx")
,告知setuptools ,将cpi.pyx 按c来编译。
第2步,执行编译
python setup.py build_ext --inplace
这命令会调用C++编译器将 .c文件编译为可执行文件,保存在当前目录下扩展名为.pyd文件( linux操作系统下编译为.so文件)。
3. 运行 cython 函数
1) 导入 cython 模块
编写1个python测试函数,导入编译后的cython模块, 用 cProfile 模块收集运行时间
# test_cpi.py
import cpi
import cProfile
cProfile.run('cpi.approx_pi()')
2) 运行cython 函数
执行 test_cpi.py,总耗时 0.042秒
python test_cpi.py
Approximate value of pi is: 3.1415925580959025
5 function calls in 0.042 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.042 0.042 <string>:1(<module>)
1 0.042 0.042 0.042 0.042 cpi.pyx:15(approx_pi)
1 0.000 0.000 0.042 0.042 {built-in method builtins.exec}
1 0.000 0.000 0.042 0.042 {cython_demo.cpi.approx_pi}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
4. 与纯python函数进行性能比较
1) 运行纯python函数
执行python函数,总耗时 6.287秒,
python pi.py
Approximate value of pi is: 3.1415925580959025
10000005 function calls in 6.287 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 6.287 6.287 <string>:1(<module>)
10000000 3.944 0.000 3.944 0.000 pi.py:2(recip_square)
1 2.343 2.343 6.287 6.287 pi.py:5(approx_pi)
1 0.000 0.000 6.287 6.287 {built-in method builtins.exec}
1 0.000 0.000 0.000 0.000 {built-in method builtins.print}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
2) 比较结果
结果相比较, 6.287 / 0.042 = 150 倍
这个计算圆周率的函数,用cython语法简单改写后,性能提升了150倍。
5. 总结
从示例可以看出,密集运算的函数经过cython简单修改为用C类型申明后,并没有增加更多代码,但速度提升非常明显,而且我们不需要C++的IDE开发环境。
因此说,Cython是提升Python程序性能的最佳方式,因此值得深入学习,并在实际项目上运用。
使用cython的一些建议
- cython语法相比python基本语法,学习难度略微增加,在理解原理后,多做几遍练习,就能熟悉掌握。
- 通常只需要在项目的少数代码中使用cython, 主要是计算密集型函数或代码块。
- 在I/O密集型函数中使用cytthon效果不明显,或者可能没什么效果,如http消息收发,大文件读写。通过异步、多线程、多进程等技术来优化。
- 如果要深入挖掘Cython潜力,建议还是掌握C++指针、STL标准库主要数据类型Vector, List, map 等类型,结合pandans, numpy,在数值运算、图像处理中可以充分发挥Cython的性能优势。
Cython中文文档:[在线阅读(Gitee)](https://apachecn.gitee.io/cython-doc-zh/)