Python 调用 C++

Python 的代码优雅而实用,但是经常会遇到性能问题,这时可以使用 C/C++ 重写几个函数,然后再用 Python 调用,这样就同时兼顾了开发效率和性能。

本文分为3个部分

  1. 安装 Cython (注意区别 CPython)
  2. Python 调用 C++ 函数
  3. Python 调用 C++ 类

1 安装 Cython

安装 Cython 最简单方法的是使用:

pip install cythonconda 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from random import random
from time import time

e = 2.7182818284590452353602874713527

def sinh(x):
return (1 - (e ** (-2 * x))) / (2 * (e ** -x))

def cosh(x):
return (1 + (e ** (-2 * x))) / (2 * (e ** -x))

def tanh(x):
return sinh(x) / cosh(x)

data = [random() for i in range(1000000)] # 生成随机数据

start_time = time() # 调用1000000次tanh函数并统计时间
result1 = list(map(tanh, data))
end_time = time()
print(end_time - start_time)

运行该程序需要 1.39 秒,接下来将上述函数改写成 C++,将其保存在 mytanh.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <cmath>

const double e = 2.7182818284590452353602874713527;

double mysinh(double x)
{
return (1 - pow(e, (-2 * x))) / (2 * pow(e, -x));
}

double mycosh(double x)
{
return (1 + pow(e, (-2 * x))) / (2 * pow(e, -x));
}

double mytanh(double x)
{
return mysinh(x) / mycosh(x);
}

由于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
2
3
4
5
6
7
8
# distutils: language = c++
# cython: language_level = 3

cdef extern from "mytanh.cpp":
double mytanh(double x)

def fast_tanh(double x):
return mytanh(x)

下面我们来解释每条语句的作用。其中

1
2
# distutils: language = c++
# cython: language_level = 3

这两行注释是用于配置编译器的特殊注释,分说明了使用的是 C++ 和 Python3。

1
2
cdef extern from "mytanh.cpp":
double mytanh(double x)

Cython 使用 cdef extern from 来声明一个在 C++ 中实现的函数。上述代码声明了 mytanh 函数,使其可以在 Cython 中使用。虽然 mytanh 现在可以在 Cython 中直接调用了,但 Python 并不能直接调用该函数,因此还要声明一个接口函数,命名为 fast_tahn

1
2
3
# 此函数完成了“包装”的工作,即完成了从 Python 类型到 C++ 类型的转换
def fast_tanh(double x): # def fast_tanh(x) 也是可行的
return mytanh(x)

上述代码声明了一个接口函数,前面所述的“包装”工作就是这个函数完成的,完成包装后,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
2
3
4
5
6
7
8
9
10
11
12
13
from distutils.core import setup, Extension
from Cython.Build import cythonize

setup(ext_modules=cythonize(Extension(
'fast_tanh', # 生成的模块名称
sources=['fast_tanh.pyx'], # 要编译的文件
language='c++', # 使用的语言
include_dirs=[], # gcc的-I参数
library_dirs=[], # gcc的-L参数
libraries=[], # gcc的-l参数
extra_compile_args=[], # 附加编译参数
extra_link_args=[], # 附加链接参数
)))

其他参数可以根据需要添加,如果你暂时还不知道这些参数有什么用,那么可以先空着。将上述代码保存到 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
2
3
4
5
6
7
8
9
10
from random import random
from time import time
from fast_tanh import fast_tanh

data = [random() for i in range(1000000)] # 生成随机数据

start_time = time() # 计算并统计时间
result = list(map(fast_tanh, data))
end_time = time()
print(end_time - start_time) # 输出运行时间

测试运行速度,运行上述代码共需 0.18 秒,可以看到,仅仅替换了一个 tanh 函数后性能提升了近 8 倍。如果有其他更复杂的操作,可以提升几十倍甚至上百倍的性能。

3 Python 调用 C/C++ 类

前面我们调用了 C/C++ 的函数,但如果我们有更多内容需要用 C/C++ 重写,想调用 C++ 编写的类呢?其实方法大同小异,主要的步骤还是在 Cython 中声明后,进行“包装”,编译,最后就可以在 Python 中调用了。

3.1 编写一个 C++ 类

我们以一个简单的矩形类为例,假设我们在 C++ 中编写了一个矩形类。头文件 Rectangle.h 为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef RECTANGLE_H
#define RECTANGLE_H

namespace shapes
{
class Rectangle
{
public:
int x0, y0, x1, y1; // 矩形对角线上的两个点坐标
Rectangle();
Rectangle(int x0, int y0, int x1, int y1);
~Rectangle();
int getArea();
void getSize(int* width, int* height);
void move(int dx, int dy);
};
}
#endif

Rectangle.cpp 中的实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include "Rectangle.h"
namespace shapes {
// 构造函数
Rectangle::Rectangle () {}
Rectangle::Rectangle (int x0, int y0, int x1, int y1) {
this->x0 = x0;
this->y0 = y0;
this->x1 = x1;
this->y1 = y1;
}
// 析构函数
Rectangle::~Rectangle () {}
// 获取矩形面积
int Rectangle::getArea () {
return (this->x1 - this->x0) * (this->y1 - this->y0);
}
// 获取矩形的边长
void Rectangle::getSize (int *width, int *height) {
(*width) = x1 - x0;
(*height) = y1 - y0;
}
// 移动矩形
void Rectangle::move (int dx, int dy) {
this->x0 += dx;
this->y0 += dy;
this->x1 += dx;
this->y1 += dy;
}
}

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
2
3
4
5
6
7
8
9
10
11
cdef extern from "Rectangle.cpp":
pass
# 用cdef声明类
cdef extern from "Rectangle.h" namespace "shapes":
cdef cppclass Rectangle:
Rectangle() except +
Rectangle(int, int, int, int) except +
int x0, y0, x1, y1
int getArea()
void getSize(int* width, int* height)
void move(int, int)

由于 .h 文件中没有实现矩形类,还要使用下面的语句来包含 Rectangle.cpp 中实现的代码

1
2
cdef extern from "Rectangle.cpp":
pass

cdef cppclass Rectangle 声明了一个在 C++ 中定义的类,其他函数的声明与前面调用函数类似。在构造函数后加上 except + 可以使 Python 能够捕获到在构造函数中发生的异常,若不加 except +,则 Cython 不会处理构造函数中发生的异常。

3.3 在 Cython 中编写接口类

与前面相同,虽然现在 C++ 中的类在 Cython 中可以直接访问了,但在 Python 中并不能访问。因此,我们还需要实现一个接口类,用于在 Python 中调用。注意,C++ 类的声明放在 .pxd 文件中, 接口类的实现放在 .pyx 中。PyRectangle.pyx为 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# distutils: language = c++
from Rectangle cimport Rectangle

# 接口类
# Python可以直接访问接口类,接口类可以直接访问C++类
cdef class PyRectangle:
cdef Rectangle c_rect # 存储C++对象
def __cinit__(self, int x0, int y0, int x1, int y1):
self.c_rect = Rectangle(x0, y0, x1, y1)
def get_area(self):
return self.c_rect.getArea()
def get_size(self):
cdef int width, height
self.c_rect.getSize(&width, &height)
return width, height
def move(self, dx, dy):
self.c_rect.move(dx, dy)

现在,PyRectangle 类就像普通的 Python 类一样可以直接在 Python 中调用了。

另外,Cython 也支持使用 new 创建 C++ 对象

1
2
def __cinit__(self, int x0, int y0, int x1, int y1):
self.c_rect = new Rectangle(x0, y0, x1, y1)

与 C++ 相同,使用了 new 就必须使用 delete 释放内存,否则会造成内存泄漏。

1
2
def __dealloc__(self):    # 析构函数
del self.c_rect # 释放内存

3.4 编译

setup.py 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
from distutils.core import setup, Extension
from Cython.Build import cythonize

setup(ext_modules=cythonize(Extension(
'PyRectangle', # 生成的模块名称
sources=['PyRectangle.pyx'], # 要编译的文件
language='c++', # 使用的语言
include_dirs=[], # gcc的-I参数
library_dirs=[], # gcc的-L参数
libraries=[], # gcc的-l参数
extra_compile_args=[], # 附加编译参数
extra_link_args=[], # 附加链接参数
)))

使用 python setup.py build_ext --inplace 编译

3.5 在 Python 中调用接口类

现在,PyRectangle 类就和普通的 Python 类一样,可以直接被 Python 调用

1
2
3
4
5
import PyRectangle

x0, y0, x1, y1 = 1, 2, 3, 4
rect = PyRectangle.PyRectangle(x0, y0, x1, y1)
print(rect.get_area())

运行该程序,输出了矩形面积,调用成功。

4 总结

通过 Cython 调用 C/C++ 的原理是:

Python -> Cython 接口 -> C/C++

访问 C++ 都是通过 Cython 接口完成的。

若还想了解更多,可以阅读 Cython 文档

5 参考资料

  1. https://www.bookstack.cn/read/cython-doc-zh/README.md
  2. https://www.youtube.com/watch?v=D9RlT06a1EI&t=45s
原创技术分享,您的支持将鼓励我继续创作