本文按照工具书逻辑罗列了Cython语法,并总结了我工作中的编程经验。对于一些常见问题给出了一些”非专业“解决办法(不知道为啥能解决,但是能解决)。由于不是科班出身,班门弄斧还请见谅,大佬们发现啥错误欢迎在下方评论,不定期修改,不定期增加内容。下面进入主题:
问题一:为什么要用Cython
Python编写数值计算程序用到了大量**for-loop,**并且这些loop无法用数组型语法替代(即不能通过Numpy、Cuda等库提升速度)。
为什么不考虑使用pypy和numba?
在我做过的测试中,pypy的性能提升弱于精心编写的Cython程序,并且对开源库支持有限(如Numpy、Matplotlib)。Cython一次编译完可重复使用。(暂不清楚有没有预先编译好的功能)
**为什么不考虑用C和C++编写好模块然后用python调用?**答案–>时间成本。如果没学过这两个语言且没有编程基础,解决问题的大部分时间都会花在语言上。C和C++各有优势,C倾向于过程,C++倾向于对象,而cython既能实现过程加速也能用类python语法完胜面向对象编程。(当然在一些细枝末节的事上,Cython肯定比不上C和C++)
问题二:使用Cython要储备好哪些知识
import numpy as np
# 不用numpy不加这行
from distutils.core import setup
# 必须部分
from distutils.extension import Extension
# 必须部分
from Cython.Distutils import build_ext
# 必须部分
ext_modules = [Extension("filename", ["filename.pyx"], include_dirs=[np.get_include()]),]
# filename就是你的.pyx文件前面的名字,注意后面中括号和这里的名字“最好”一致,不然有些时候会报错。
# 调用numpy就添加include_dirs参数,不然可以去掉
setup(name="a faster version", cmdclass={"build_ext": build_ext}, ext_modules=ext_modules)
# 这个name随便写,其他的格式不能变
# (更新一下,python3.8的时候这个名字要和上面的filename一样)
\3. 项目文件夹路径下的命令行中输入以下指令(Pycharm下面有个terminal,点开默认位置是项目的顶层文件夹,可以cd到Cython文件文件夹)
python3.6 setup.py build_ext --inplace
# 注意3.6是python的版本号
# setup.py 和 build_ext 是必须参数
# --inplace的作用是把生成的cython可执行文件放在当前文件夹下
\4. 成功运行后会生成一些文件包括:build文件夹、.c文件、.so文件。build文件夹和c文件都可以删除,.so文件里面是.pyx编译好的可供python调用的文件。调用.pyx的格式和调用普通python文件中函数的格式相同。能够调用则编译成功。
from filename import funcname
Win10系统的编译流程和Mac os略有区别,所以在Mac os上编译好的cython文件是不能直接在windos系统里用的,需要跨平台的话就要在两边都编译一下。在windos下会生成一个.pyd文件,这个文件的主要功能是协调文件和模块调用,并不影响其他程序。
编译的指令也略有区别(取消了python后面的版本号,其他相同)
python setup.py build_ext --inplace
Cython的加速可以逼近C,也就是说在程序关键部位越少地调用或不调用python解释器才能在性能上得到大幅度提升。那么如何检查自己编写的Cython代码与Python解释器的关联情况呢?在文件路径下的命令行中输入下方指令:
cython -a filename.pyx
运行后会在该文件夹下生成一个.html文件(在pycharm中打开这个文件,右上角会出现小浏览器图标,点击后即可查看)。文件会为每一行代码标色,白色为纯C代码, 黄色为调用了Python解释器的代码,黄色越重,这行代码的加速效果就越差。
关于如何把黄色变浅,后面会根据不同的原因给出解决办法。
此外这行指令还会检查你的Cython基本语法格式是否正确(不考虑运行中的调用问题,只检查格式)。如果不正确则会报出错误情况,并且不会生成html代码。
声明变量类型
cdef int i # 整型
cdef long j # 长整型
cdef double a # 浮点数
cdef object p # python对象(速度很慢,不到没办法不要用)
cdef double a,b=2.0, c=3.0 # 多变量在同行定义
变量类型转换
cdef int a = 0
cdef double b
b = <double> a # 其他类型转换也适用
声明变量类型是逼近C性能的重要环节,在主loop中的所有变量都要声明类型,这样可以保证循环过程中不调用python解释器。通常情况下需要声明的有数组、循环变量i、其他中间计算参数等。
如果html文件中某一行出现重黄色,很可能是因为没有声明变量类型
无输入参数,返回一个整数,只能在cython文件内部调用
cdef int function():
return 0
无输入参数,返回一个整数,既能在cython中调用,也能在python中调用。
cpdef double function():
return 1.0
cpdef生成供Cython调用和供Python调用的两个接口。因为cdef函数无法被该文件外的.py文件调用,所以cpdef函数相当于连接Cython文件与Python文件的桥梁。由于生成了Python的接口,所以html文件中这一行不可避免地会变黄,在函数不被反复调用的情况下不用在意这点性能损失。
有输入参数,无返回值,只能在cython文件内部调用
cdef int function(int a, int b, double[:, :] c, int[:] d):
return 0
没有返回值还要return 0的原因是函数必须有且只能有一个返回参数,如果不指定返回值的话会识别为Python语法函数设定返回None,这样又会调用Python解释器,所以用0占位。(土办法别笑话我,有更好的方案大神们请评论)。 传参时要指定变量类型,下面函数中前两个参数是整数,后面是浮点数数组视图和整数数组视图,相当于Numpy的ndarray。不过现在不懂不要担心,后面会说,这里只是举个例子。
有输入参数,有返回值,内联函数
cdef inline int function(int a, int b):
return a+b
内联函数调用开销小,所以函数内容很少时使用内联函数。编译时会把这个函数直接替换到代码的对应位置,减少函数调用开销。
虽然Python的list很灵活,但是因为速度慢所以在Cython代码的for-loop中尽量不用Python的列表。Cython有两种定义数组的方式:1.定义C数组、2.定义Numpy数组。相比于列表,这两种数组内只能有一种数据类型且数据在内存中是连续存储的。
定义C数组
cdef double arr[10] # 定义一个叫arr的一维数组,尺寸为10
cdef double arr[5][2] # 定义一个叫arr的二维数组,尺寸为(5,2)
定义Numpy数组
arr = np.zeros(10, dtype=np.float64) # 定义numpy一维数组,float64即double
定义Numpy数组必然会调用Python解释器,所以不要在大量重复执行的位置使用。一般在开始循环前预先为数组分配好内存空间。
内存视图
for-loop中操作Numpy数组也要调用Python解释器,所以进入循环前需要对Numpy数组做转换处理。Cython提供了”内存视图“用于直接访问Numpy数组存储的数据,这样可以跳过一些冗余处理直达数据,因此也可以跳过Python解释器。
cdef double[:] arr # 定义一个名为arr的一维数组, 类型为double
cdef double[:, :] arr # 二维数组
cdef int[:, :, :] arr # 三维数组,类型为int
cdef long[::1] arr # 一维连续存储数组
cdef double[:, ::1] arr # 二维数组,1轴上数据连续
cdef double[:, :, ::1] arr # 三维数组,2轴上数据连续
cdef double[::1, :] arr # 二维数组,0轴上数据连续
定义内存视图时数组是几维就用几个冒号占位。如定义C类型视图,最后一个冒号可以更改为::1即声明最后一个维度的数据是连续的(声明会提高计算效率,不声明也可)。如定义Fortran类型视图,第一个冒号可以更改为::1,原理与上相同。需要注意内存视图的数据类型与维度必须与后面赋值的Numpy数组严格相同,否则会出错。
内存视图对接Numpy数组
cdef double[:, ::1] arr
arr = np.zeros([5, 5], dtype=np.float64)
print(arr[2, 2])
print(arr.shape)
print(arr.base)
前两行代码可以让内存视图对接Numpy数组的数据,可以简单理解为在Cython中,内存视图就是Numpy数组。在for-loop中可用下标访问内存视图的数据,操作为C级别,与Python无关。
内存视图有shape和base两个属性,shape和Numpy的shape一样为数组的形状,base即是该内存视图所代表的Numpy数组,一般把数组传回python程序时,不直接传视图,应该传视图.base。
从便捷性来讲,尽量用Numpy定义,不要用C定义,除非大批量定义。
由于Cython还是基于Python的语法做转换的,在转换的过程中难免会涉及到一些检查操作,这些检查操作也是由Python解释器完成的,在for-loop中执行这些检查会严重影响性能。如果能够保证代码的正确性,可以强制关闭这些检查提升速度。可选择内容如下。
boundscheck # 数组下标越界
wraparound # 负索引
cdivision # 除0检查
initializedcheck # 内存视图是否初始化
有两种方法使用这些检查,一种是在文件头做全局关闭,另一种是以装饰器的形式关闭某个函数内的检查。False为关闭,True为打开。
# cython: initializedcheck=False
# cython: cdivision=True
# cython: boundscheck=False
# cython: wraparound=False
@cython.boundsback(False)
def function(...):
using deprecated numpy
# 使用cimport numpy调用了numpy,该错误不影响程序运行
skipping "filename.c"cython extension (up-to-date)
# setup.py文件中ext_modules里的两个name不同
Unable to find vcvarsall.bat
# 缺少与C++有关的环境,安装Visual Studio的C++组件
在Cython中调用某库函数(如time)编译报错时,可以在Python文件中调用然后把函数名作为cpdef的参数传递进去