Python 解释器
+正如软件系统通常分为定义与实现两部分,编程语言也是。
+-
+
-
+
比如 C++ 标准是由国际标准化组织 ( ISO ) 下属的
+ISO/IEC JTC1/SC22/WG21
特别工作组制定的,最新标准为 C++ 20 (ISO/IEC 14882:2020),这是定义;而其实现则有 GNU 的 gcc、微软的 visual c++、Apple 发起的 clang 等等,任何人都可以按照定义去实现自己的实现。
+ -
+
Java 规范则是由 Oracle 主导的 JCP 所制定,有 Oracle Java SE、OpenJDK、Corretto、AdoptOpen 等大量不同的实现。
+
+ -
+
更别论应用更为广泛的 JavaScript 语言,ECMAScript 的实现者至少有两位数。
+
+ -
+
Python 作为一门广泛使用坐拥庞大社区支持的编程语言,更是如此。
+
+
Python 的语言规范是由 Python 软件基金会(PSF)负责制定的。
+不同于 ISO(国际标准化组织)或 ECMA(欧洲计算机制造商协会)这些更为正式的标准化机构,Python 的规范制定过程相对非正式,主要由社区驱动。
+与严格的书面标准相比,Python 标准给实现者留下了更多的灵活性和创造空间。
+Python 编程语言的“实现” 通常被称为解释器,由 PSF 官方维护的基于 C 语言实现解释器 CPython 是使用最为广泛的解释器,也是大家默认 Python 实现。
+除此之外,还有:
+-
+
- 基于 JVM 实现的
Jython
graalpython
,方便与 Java 编程语言集成
+ - 基于 .NET 框架实现的
Python.NET
IronPython
+ - 基于 Python ( RPython,Python 语法子集)自举实现的 JIT 解释器
PyPy
+ - 适合于 IOT 场景的 MicroPython,裁减了部分标准库优化了其资源占用 +
- 哪里有 C 代码,哪里就有人在用 Rust 重写:RustPython +
回到正题,由于我们默认的 Python 解释器 CPython 就是基于 C 语言实现的,所以其与 C/C++ 系语言天然就具有较高的亲和性,这正是我们能顺畅地在 C++ 中调用 Python 的理论基础所在。
+在本文的后续部分,除非另有说明,提及的“解释器”均指的是由 Python 软件基金会官方维护的 CPython 解释器。
+Python Extension Module
+具体到 Python 是如何与 C++ 集成的,这就不得不提到 Extension Module 机制了。
+众所周知,模块是 Python 代码组织中的一个重要单元,用于封装和重用代码,我们会通过 import
关键字引入一个模块从而可以调用外部的代码。
而 Python 的 Extension Module 机制则允许我们利用 C/C++ 去实现 Python module,C/C++ 中的函数会按照约定的签名映射成 Python 中的函数。
+代码开发完成之后,我们需要将之编译成一个动态链接库,Python 解释器在 import
的时候会在PYTHONPATH
以及一些约定的目录下,按照约定的名称进行搜索,解释器导入模块的查找顺序如下,以 import foo
为例:
-
+
foo.py
+- foo.pyc (如果有编译过的字节码文件存在) +
foo.so
(在 Unix-like 系统)
+foo.pyd
(在 Windows 系统,相当于 DLL)
+_foo.so
(可能的 C 扩展模块名字)
+foo.cpython-36m-x86_64-linux-gnu.so
(可能的带有版本号和架构的 C 扩展文件名)
+
import
之后,在 Python 代码中,我们就可以像调用普通 Python 模块一样调用我们的 C/C++ 代码了。
举个完整的例子,假设我们想要实现一个加法模块,以下是相关操作步骤。
+编辑 add_module.c
文件:
// 必须要 include python.h 头文件
+#include <Python.h>
+
+// C 函数实现加法
+static PyObject* add(PyObject* self, PyObject* args) {
+ double a, b;
+ // 解析传入的Python参数为C类型变量,dd 表示两个 double 类型变量
+ if (!PyArg_ParseTuple(args, "dd", &a, &b)) {
+ return NULL;
+ }
+ // 返回计算结果,d 表示返回一个 double 类型的变量
+ return Py_BuildValue("d", a + b);
+}
+
+// 定义模块要导出的方法列表
+static PyMethodDef AddMethods[] = {
+ {"add", add, METH_VARARGS, "Add two numbers"},
+ {NULL, NULL, 0, NULL} // Sentinel,标记数组的结束(C 数组无法直接获取长度,解释器会通过 Sentinel 来判断数组结尾)
+};
+
+// 定义模块
+static struct PyModuleDef addmodule = {
+ PyModuleDef_HEAD_INIT,
+ "add_module", // 模块名
+ NULL, // 模块文档,可以设置为 NULL
+ -1, // 模块保留大小,-1 表示模块保持全局状态
+ AddMethods // 方法列表
+};
+
+// 初始化模块
+PyMODINIT_FUNC PyInit_add_module(void) {
+ return PyModule_Create(&addmodule);
+}
+
为了让这个 C 代码可以被 Python 解释器识别,我们需要将其编译成一个动态链接库:
+gcc -Wall -shared -fPIC -I/usr/include/python3.9 -L/usr/lib/python3.9/config-x86_64-linux-gnu -lpython3.9 add_module.c -o add_module.so
+
此时我们就得到了一个 add_module.so
文件,直接在 Python 代码中 import
即可使用:
root@ubuntu:/ # python3.9
+Python 3.9.18 (main, Feb 26 2024, 01:38:59)
+[GCC 11.4.0] on linux
+Type "help", "copyright", "credits" or "license" for more information.
+>>> import add_module
+>>> print(add_module.add(6.6, 6.6))
+13.2
+>>>
+
在科学计算和人工智能领域,大量的 Python 库都在通过这种方式兼顾效率与性能,如:NumPy
、Pandas
、Tensorflow
、PyTorch
等等。
C++ -> Python
+将 C++ 代码按照约定的方式编译即能让 Python 解释器识别,实现 Python 对于 C++ 代码的调用,那么反过来 C++ 该如何调用 Python 代码呢?
+在 C/C++ 中调用 Python 解释器运行,这种机制被称为 Embedding Python,其实运作原理也是和 Extension Module 非常类似的。
+Python 解释器提供了一套 API ( Python/C API ),供 C/C++ 代码调用,C/C++ 可以借由这套 API 实现对于解释器的高效集成与复杂交互操作,包括导入模块、创建和操作 Python 对象、函数调用、传输与转换数据等等,几乎所有 Python 代码能实现的操作,也能通过这套代码在 C/C++ 中实现。
+举个例子,科学计算正是 Python 语言的强项之一,而 NumPy 是一个非常著名的 Python 科学计算库,我们可以尝试通过 Python/C API 将这种能力导入到 C/C++ 程序中,numpy.linalg.norm
函数可以计算一个一维数组对应的欧几里得范数,下文将介绍如何将该函数封装到 C/C++ 程序中;
由于在复杂场景下 C 代码可读性要弱于 C++ 代码,下文案例采用 C++ 实现:
+#include <Python.h> // Python 头文件
+#include <vector>
+#include <iostream>
+#include <stdexcept>
+
+// C++ 包装函数
+double numpy_linalg_norm(const std::vector<double> &in_array) {
+ Py_Initialize(); // 初始化 Python 解释器
+
+ PyObject *pName = PyUnicode_FromString("numpy.linalg"); // 加载 NumPy 模块
+ PyObject *pModule = PyImport_Import(pName);
+ Py_DECREF(pName); // 释放 pName(Python 引用计数 -1)
+
+ if (pModule == NULL) {
+ PyErr_Print();
+ Py_Finalize();
+ throw std::runtime_error("Failed to load numpy.linalg module");
+ }
+
+ PyObject *pFunc = PyObject_GetAttrString(pModule, "norm"); // 获取 norm 函数
+ if (!pFunc || !PyCallable_Check(pFunc)) {
+ PyErr_Print();
+ Py_Finalize();
+ throw std::runtime_error("Failed to retrieve norm function");
+ }
+
+ PyObject *pList = PyList_New(in_array.size()); // 创建一个 Python 列表
+ for (size_t i = 0; i < in_array.size(); ++i) { // 将 C++ 数组转换为 Python 列表
+ PyList_SetItem(pList, i, PyFloat_FromDouble(in_array[i]));
+ }
+
+ PyObject *pArgs = PyTuple_New(1); // 创建参数元组
+ PyTuple_SetItem(pArgs, 0, pList);
+
+ PyObject *pValue = PyObject_CallObject(pFunc, pArgs); // 调用函数
+ Py_DECREF(pArgs);
+ if (pValue == NULL) { // 判断是否成功调用
+ PyErr_Print();
+ Py_Finalize();
+ throw std::runtime_error("Failed to call norm function");
+ }
+
+ double result = PyFloat_AsDouble(pValue); // 处理返回值
+
+ Py_DECREF(pValue);
+ Py_DECREF(pFunc);
+ Py_DECREF(pModule);
+
+ Py_Finalize(); // 关闭 Python 解释器
+
+ return result;
+}
+
+int main() {
+ try {
+ std::vector<double> in_array = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
+ double norm = numpy_linalg_norm(in_array);
+ std::cout << "L2 Norm of the array is: " << norm << std::endl;
+ } catch (const std::exception &e) {
+ std::cerr << "An error occurred: " << e.what() << std::endl;
+ return 1;
+ }
+
+ return 0;
+}
+
这里由于依赖到了 numpy
库,所以我们需要事先通过 Python 的包管理程序安装它:
python3.9 -m pip install numpy
+
然后保存这段代码,编译运行就能在标准输出中看到这个数组对应的欧几里得范数了:
+g++ -o cpp_call_python_demo main.cpp -I/usr/include/python3.9 -lpython3.9
+
Cython
+尽管 Python/C API
为 C/C++ 调用 Python 提供了理论上的可行性,但是我们通过上方的案例也不难看出,其复杂程度是相当高的,即使 Python 中诸如赋值、初始化这样简单的操作都需要映射成 C/C++ 中的一个函数,另外还需要进行相当频繁地错误判断与处理,手动引用释放等逻辑,复杂程序不亚于在二十一世纪手写汇编程序。
为了实现 numpy.linalg.norm([1, 2, 3, 4, 5, 6])
这一行代码,我们总共写了整整五十行代码,如果再多几行 Python 代码想必 C/C++ 程序的复杂度更是指数级爆炸增长。
Obviously,我们还需要一层”抽象“,而 Cython 可能正是我们寻找的答案。
+++The most widely used Python to C compiler.
+
在 Cython 的 Github 主页,它是这么介绍自己的。
+让我们来分析一下这句话,compiler
是宾语,意味着 Cython 本质上是一个编译器,而这个编译器的定语Python to C
则表明它这个编译器的输入是 Python 语言,而输出是 C 语言,这句话向我们精炼地概括了 Cython 的核心特性。
Cython 官网首页的这一段话,则是其更加详细地介绍:
+++The Cython language is a superset of the Python language that additionally supports calling C functions and declaring C types on variables and class attributes.
+
这句话中的宾语则是”编程语言“,可能会让人有点困惑,不过正如第一节中所描述的: +”编程语言也是由定义与实现两部分组成的“,Cython 也是。
+Cython 本质上是一门编程语言,在 Python 语法的基础上,加入了对于 C 语法的支持,使得它能非常恰当的胜任两门语言中的那一层胶水,不管是通过 Cython 开发 Extension Module,或者是 Embedding 到 C/C++ 程序中,都能省事很多。
+Cython 代码通常是以 .pyx
结尾,.pxd
文件则类比于 C 语言中的 .h
头文件,用于存放结构定义,前面提到 Cython 是一个编译器,所以这门编程语言是一门编译型语言,Cython 编译器会每一个源代码文件编译成一个 C 或者 C++ 文件,这取决于我们的配置;
然后我们需要再借助 C/C++ 编译器的帮助,将它们进一步编译成包含机器码指令的动态链接库,这就是我们最终的产物了,这个动态链接库是一个非常神奇的东西,胶水两边的语言都能无缝调用。
+首先它是符合 Python Extension Module 规范的,所以能被纯 Python 代码 import
,其次它还能暴露 public
的 C/C++ 函数,所以也能在 C/C++ 程序中被调用,在两边代码的调用者视角中,就是简简单单引了个库,调个函数它就能把事给办了,不论在哪一边都是妥妥的一等公民。
作为一门编程语言其语法细节当然也不是简单几笔就能概括完的,这里就不展开了,我们还是通过一个程序案例的方式来直观地感受一下。
+这里我们直接将上文基于 Python/C API 实现的 demo,转换成用 Cython 实现:
+首先我们需要安装 Cython 编译器:
+python3.9 -m pip install cython
+
编辑文件 numpy_wrapper.pyx
:
# distutils: language = c++
+
+import numpy
+from libcpp.vector cimport vector # Cython 内置了对于部分 C++ STL 结构的封装
+
+# 申明为 public 表明该函数需要被导出
+cdef public double numpy_linalg_norm(const vector[double] & in_array):
+ cdef tuple py_tuple = tuple() # 将 C++ vector 转换为 Python tuple
+ cdef int i
+ for i in range(in_array.size()):
+ py_tuple += (in_array[i],)
+
+ return numpy.linalg.norm(py_tuple) # Python 调用
+
这就是我们 cython
版本的代码了,可以看到其语法风格和 Python 基本差不多。
然后我们需要配置一下构建逻辑 setup.py
:
from Cython.Build import cythonize
+from setuptools import setup
+from setuptools.extension import Extension
+
+extensions = [
+ Extension(
+ "numpy_wrapper",
+ ["numpy_wrapper.pyx"],
+ language="c++", # 使用 C++ 语言特性编译成 C++ 源文件
+ extra_compile_args=["-std=c++20"], # 指示编译器使用 C++20 标准
+ extra_link_args=["-std=c++20"],
+ )
+]
+
+setup(
+ name='numpy_wrapper',
+ ext_modules=cythonize(
+ extensions,
+ language_level=3, # 指定 Cython 编译器使用 Python 3 语法
+ )
+)
+
编译之后:python3.9 setup.py build_ext --inplace
,我们就能看到在当前文件夹下方多出来了几个文件:
root@ubuntu# ls -1
+build
+numpy_wrapper.cpp
+numpy_wrapper.cpython-39-x86_64-linux-gnu.so
+numpy_wrapper.h
+numpy_wrapper.pyx
+setup.py
+
.cpp
和 .h 是 Cython 编译的产物,这两个文件进一步调用 gcc 编译就生成了我们上文提到的动态链接库:numpy_wrapper.cpython-39-x86_64-linux-gnu.so
。
然后我们就能像 C++ 程序调用常规第三方库的方式去开发了:
+-
+
- include 这个头文件 +
- 链接这个动态链接库,不过在这之前需要按照标准的动态链接库文件名格式重命名一下 +
- libpython 库也还是照样需要引用的 +
组合一下编译命令就长这样了:
+ g++ -o cpp_call_python_demo main.cpp -I/usr/include/python3.9 -lpython3.9 -L. -lnumpy_wrapper
+
然后再简单修改一下我们的 C++ 代码就能得到同样的输出:
+#include <Python.h>
+#include <vector>
+#include <iostream>
+#include <stdexcept>
+#include "numpy_wrapper.h"
+
+// 初始化 Python 解释器,导入模块
+void initialize_py_interpreter() {
+ PyImport_AppendInittab("numpy_wrapper", PyInit_numpy_wrapper);
+ Py_Initialize();
+ PyImport_ImportModule("numpy_wrapper");
+}
+
+
+int main() {
+ initialize_py_interpreter();
+
+ try {
+ std::vector<double> in_array = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
+ double norm = numpy_linalg_norm(in_array);
+ std::cout << "L2 Norm of the array is: " << norm << std::endl;
+ } catch (const std::exception &e) {
+ std::cerr << "An error occurred: " << e.what() << std::endl;
+ return 1;
+ }
+
+ return 0;
+}
+
而当我们反过来需要在 Python 中调用 C++ 的时候,Cython 也可以帮我们免去按照约定的 Python Extension Module 开发的中间层,C++ 代码还是按照正常流程开发就行,Cython 中可以直接 include 对应的 C++ 头文件,链接对应的动态链接库,然后再暴露给纯 Python 模块调用。
+ +