Python 的代码优雅而实用,但是经常会遇到性能问题,这时可以使用 C/C++ 重写几个函数,然后再用 Python 调用,这样就同时兼顾了开发效率和性能。
本文分为3个部分
- 安装 Cython (注意区别 CPython)
- Python 调用 C++ 函数
- Python 调用 C++ 类
1 安装 Cython
安装 Cython 最简单方法的是使用:
pip install cython
或 conda install cython
Python 的程序写完了可以直接通过 python main.py
执行,但与 Python 不同,Cython 编写的程序需要编译后才能执行,因此,Cython 要求系统中有 C/C++ 编译器,gcc/g++ 或是 Visual Studio 的编译器均可。若电脑中已经安装了 Visual Studio 的 C++ 相关工具,那么电脑中就已经有 Visual Studio 的 C++ 编译器了,Cython 可以直接使用它。
2 Python 调用 C/C++ 函数
如果我们在 Python 中遇到了一个执行很慢的函数,需要用 C++ 重写这个函数,下面我们通过一个简单的例子来说明如何处理。
2.1 编写一个 Python 函数
以一个简单的函数为例,在 Python 中编写如下函数来计算 $\text{tanh}(x)$ 的值
1 | from random import random |
运行该程序需要 1.39 秒,接下来将上述函数改写成 C++,将其保存在 mytanh.cpp
中
1 |
|
由于sinh, cosh, tanh
是 C++ 库函数,为了避免命名冲突,这里修改一下函数名。
2.2 在 Cython 中声明该函数
C++ 的函数已经重写好了,下面要将 .cpp
代码进行一些“包装”,使 Python 能够调用它。这个“包装”的工作就是通过 Cython 进行的,众所周知,C++ 是静态类型语言,Python 是动态类型语言,不做任何处理,二者将不能直接调用,“包装”的主要工作其实就是完成各个变量的“类型转换”,例如将 Python 的 int
对象转为 C++ 的 int
类型,这两种 int
是不同的。
Cython 使用后缀名为 .pyx
和 .pxd
的文件,它们也是代码文件。.pyx
类似于 .cpp
,.pxd
类似于 .h
。下面进行“包装工作”,我们先不使用 .pyd
文件。
新建一个 fast_tanh.pyx
文件,文件内容如下。
1 | # distutils: language = c++ |
下面我们来解释每条语句的作用。其中
1 | # distutils: language = c++ |
这两行注释是用于配置编译器的特殊注释,分说明了使用的是 C++ 和 Python3。
1 | cdef extern from "mytanh.cpp": |
Cython 使用 cdef extern from
来声明一个在 C++ 中实现的函数。上述代码声明了 mytanh
函数,使其可以在 Cython 中使用。虽然 mytanh
现在可以在 Cython 中直接调用了,但 Python 并不能直接调用该函数,因此还要声明一个接口函数,命名为 fast_tahn
。
1 | # 此函数完成了“包装”的工作,即完成了从 Python 类型到 C++ 类型的转换 |
上述代码声明了一个接口函数,前面所述的“包装”工作就是这个函数完成的,完成包装后,Python 能直接调用的是这个 fast_tanh
函数,而不是原始的 mytanh
函数。Cython 的语法与 Python 非常相似,若去掉形参中的 double 也是可行的,但若 Cython 知道参数的类型可以加速运行速度。Cython 支持大部分普通的 Python 代码,因此可以在 Cython 中将 Python 的数据类型和 C++ 的数据类型相互转换,例如可以将 vector
转为 numpy array
。若要使用 vector
类型,还需在开头加上 from libcpp.vector cimport vector
。
2.3 编写 setup.py
fast_tanh.pyx
编写完后,需要编译后才能被 Python 调用,编译是通过 setup.py
进行的。
1 | from distutils.core import setup, Extension |
其他参数可以根据需要添加,如果你暂时还不知道这些参数有什么用,那么可以先空着。将上述代码保存到 setup.py 后,运行如下命令即可编译 Cython 文件。
1 | python setup.py build_ext --inplace |
需要注意,编译时的 Python 版本必须和调用时使用的 Python 版本相同。编译完成后,当前目录下会自动生成相应的 cpp 文件和 pyd 文件,在 Linux 上是 so 文件。
如果使用了 numpy 会在编译过程中看到警告:
1 | Warning Msg: Using deprecated NumPy API, disable it with #define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION |
该警告可以忽略,因为 Cython 使用的是已经弃用的 Numpy API,不影响使用。
2.4 在 Python 中调用 fast_tanh 函数
完成了编译的步骤后,fast_tanh
在 Python 中就和一个普通的 Python 模块一样,可以使用 import 来导入
1 | from fast_tanh import fast_tanh # 从 fast_tanh.pyx 中导入 fast_tanh 函数 |
导入后,就可以在 Python 中像调用普通函数一样,直接使用 fast_tanh
函数了,完整代码如下,与之前的区别仅仅是把 tanh
替换成了 fast_tanh
,非常方便。
1 | from random import random |
测试运行速度,运行上述代码共需 0.18 秒,可以看到,仅仅替换了一个 tanh 函数后性能提升了近 8 倍。如果有其他更复杂的操作,可以提升几十倍甚至上百倍的性能。
3 Python 调用 C/C++ 类
前面我们调用了 C/C++ 的函数,但如果我们有更多内容需要用 C/C++ 重写,想调用 C++ 编写的类呢?其实方法大同小异,主要的步骤还是在 Cython 中声明后,进行“包装”,编译,最后就可以在 Python 中调用了。
3.1 编写一个 C++ 类
我们以一个简单的矩形类为例,假设我们在 C++ 中编写了一个矩形类。头文件 Rectangle.h
为:
1 |
|
Rectangle.cpp
中的实现为:
1 |
|
3.2 在 Cython 中声明类
接下来需要在 Cython 中编写一个接口。与前面调用 C++ 函数类似,使用 cdef extern from
来声明一个在 C++ 中实现的类:
1 | cdef extern from "Rectangle.h" namespace "shapes": |
若没有命名空间,则使用:
1 | cdef extern from "Rectangle.h" |
我们现在使用 .pxd
文件,其实如果一定要像刚才一样放在一个文件里也是可以的。将声明放在 Rectangle.pxd
文件中,.pxd
文件相当于 C++ 的 .h
文件,专门用于声明:
1 | cdef extern from "Rectangle.cpp": |
由于 .h
文件中没有实现矩形类,还要使用下面的语句来包含 Rectangle.cpp
中实现的代码
1 | cdef extern from "Rectangle.cpp": |
cdef cppclass Rectangle
声明了一个在 C++ 中定义的类,其他函数的声明与前面调用函数类似。在构造函数后加上 except +
可以使 Python 能够捕获到在构造函数中发生的异常,若不加 except +
,则 Cython 不会处理构造函数中发生的异常。
3.3 在 Cython 中编写接口类
与前面相同,虽然现在 C++ 中的类在 Cython 中可以直接访问了,但在 Python 中并不能访问。因此,我们还需要实现一个接口类,用于在 Python 中调用。注意,C++ 类的声明放在 .pxd
文件中, 接口类的实现放在 .pyx
中。PyRectangle.pyx
为 :
1 | # distutils: language = c++ |
现在,PyRectangle
类就像普通的 Python 类一样可以直接在 Python 中调用了。
另外,Cython 也支持使用 new
创建 C++ 对象
1 | def __cinit__(self, int x0, int y0, int x1, int y1): |
与 C++ 相同,使用了 new
就必须使用 delete
释放内存,否则会造成内存泄漏。
1 | def __dealloc__(self): # 析构函数 |
3.4 编译
setup.py
内容如下
1 | from distutils.core import setup, Extension |
使用 python setup.py build_ext --inplace
编译
3.5 在 Python 中调用接口类
现在,PyRectangle
类就和普通的 Python 类一样,可以直接被 Python 调用
1 | import PyRectangle |
运行该程序,输出了矩形面积,调用成功。
4 总结
通过 Cython 调用 C/C++ 的原理是:
Python -> Cython 接口 -> C/C++
访问 C++ 都是通过 Cython 接口完成的。
若还想了解更多,可以阅读 Cython 文档