当前位置: 首页 > 工具软件 > Cython > 使用案例 >

Cython学习

通寂离
2023-12-01

Cython学习

本文按照工具书逻辑罗列了Cython语法,并总结了我工作中的编程经验。对于一些常见问题给出了一些”非专业“解决办法(不知道为啥能解决,但是能解决)。由于不是科班出身,班门弄斧还请见谅,大佬们发现啥错误欢迎在下方评论,不定期修改,不定期增加内容。下面进入主题:


Cython:疑问篇

问题一:为什么要用Cython

  1. Python编写数值计算程序用到了大量**for-loop,**并且这些loop无法用数组型语法替代(即不能通过Numpy、Cuda等库提升速度)。

  2. 为什么不考虑使用pypy和numba?

    在我做过的测试中,pypy的性能提升弱于精心编写的Cython程序,并且对开源库支持有限(如Numpy、Matplotlib)。Cython一次编译完可重复使用。(暂不清楚有没有预先编译好的功能)

  3. **为什么不考虑用C和C++编写好模块然后用python调用?**答案–>时间成本。如果没学过这两个语言且没有编程基础,解决问题的大部分时间都会花在语言上。C和C++各有优势,C倾向于过程,C++倾向于对象,而cython既能实现过程加速也能用类python语法完胜面向对象编程。(当然在一些细枝末节的事上,Cython肯定比不上C和C++)

问题二:使用Cython要储备好哪些知识

  1. 如果只会Python一门编程语言,不建议直接用Cython,不然很多问题没有解决思路(当然如果你能耐心看完这篇文章可以省很多事)。较好的顺序是先看C基础(至少看完循环、条件分支与函数),然后学习Cython,这样应用时的效率会高很多。

Cython:编译流程(Mac os)

  1. 在项目文件夹下新建一个后缀名为.pyx的文件即Cython文件,如果有写好的python代码可以把代码先复制过去。
  2. 在项目文件夹下新建一个名为setup.py的python文件,在里面敲好如下代码:
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

Cython:编译流程(Win10)

Win10系统的编译流程和Mac os略有区别,所以在Mac os上编译好的cython文件是不能直接在windos系统里用的,需要跨平台的话就要在两边都编译一下。在windos下会生成一个.pyd文件,这个文件的主要功能是协调文件和模块调用,并不影响其他程序。

编译的指令也略有区别(取消了python后面的版本号,其他相同)

python setup.py build_ext --inplace

Cython:性能分析与语法预检

Cython的加速可以逼近C,也就是说在程序关键部位越少地调用或不调用python解释器才能在性能上得到大幅度提升。那么如何检查自己编写的Cython代码与Python解释器的关联情况呢?在文件路径下的命令行中输入下方指令:

cython -a filename.pyx

运行后会在该文件夹下生成一个.html文件(在pycharm中打开这个文件,右上角会出现小浏览器图标,点击后即可查看)。文件会为每一行代码标色,白色为纯C代码, 黄色为调用了Python解释器的代码,黄色越重,这行代码的加速效果就越差。

关于如何把黄色变浅,后面会根据不同的原因给出解决办法。

此外这行指令还会检查你的Cython基本语法格式是否正确(不考虑运行中的调用问题,只检查格式)。如果不正确则会报出错误情况,并且不会生成html代码。


Cython:语法(变量)

声明变量类型

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:语法(函数)

无输入参数,返回一个整数,只能在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

内联函数调用开销小,所以函数内容很少时使用内联函数。编译时会把这个函数直接替换到代码的对应位置,减少函数调用开销。


Cython:语法(数组)

虽然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:强制关闭一些非必要操作

由于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(...):

Cython:报错解决方案

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:使用经验

在Cython中调用某库函数(如time)编译报错时,可以在Python文件中调用然后把函数名作为cpdef的参数传递进去

 类似资料: