我们知道 C++代码的执行效率大多数情况下都会优于 Python 代码。当我们开发一个 Python 工具,分享时,使用者就可以通过 pip install xxx 的方式安装我们的工具,我们将该工具包中某些运行效率太低的部分,使用 C++ 来编写。
我们实现一个 my_test 工具包,该包中一部分代码我们用 Python 来实现,一部分代码使用 C++ 来实现。最终能够实现在 Python 中正常调用两部分函数。整个项目结构如下:
. |-- README.md |-- my_test | |-- __init__.py | |-- cpp | | |-- __init__.py | | |-- c_function_wrapper.py | | `-- c_module | | |-- def_module.cpp | | |-- my_function.cpp | | `-- my_function.h | `-- python | |-- __init__.py | `-- function.py `-- setup.py
从文件结构来看,我们发现整个项目中既包含 python 代码,也包含 cpp 代码。我们接下来,详细了解下每一部分怎么编写,以及如何将其打包成最终的工具包,并能够通过 pip install 安装到电脑上,并调用其中的 python 和 cpp 函数。
1. 编写 Python 部分函数
my_test 是我们最终要打包的目录,在 my_test 目录下的 python 目录下的两个文件内容分别为:
__init__.py
from my_test.python.function import *
function.py
def function1(): return "Hello MyTest!" def function2(my_list: list) -> list: for i in range(len(my_list)): my_list[i] += 100 return my_list def function3(a: int, b: int) -> int: return a + b if __name__ == '__main__': print(function1()) print(function2([1, 2, 3])) print(function3(10, 20))
这部分很简单,不做过多解释。
2. 编写 CPP 部分函数
在 cpp 不分,我们有两部分:
- my_function.h 和 my_function.cpp 用于定义 cpp 函数
- def_module.cpp 定义了根据前面那俩 cpp 文件生成模块的信息。
- 上面的 cpp 文件编译完之后,会生成一个动态库文件,在 mac 上扩展名 so 文件。在 Python 中可以直接导入该模块使用。我们这里做一个调用的包装,在 c_function_wrapper.py 中调用了该 so 文件中 cpp 函数。
2.1 my_function.h
#ifndef MY_FUNCTION_H #define MY_FUNCTION_H #include <Python.h> #ifdef __cplusplus extern "C" { #endif PyObject* function1(PyObject* self); PyObject* function2(PyObject* self, PyObject* args); PyObject* function3(PyObject* self, PyObject* args, PyObject* kwargs); #ifdef __cplusplus } #endif # endif
2.2 my_function.cpp
#include "my_function.h" /* 格式化字符串 C 数据类型 Python 对象类型 "i" int int "l" long int 或 long "f" float float "d" double float 或 decimal.Decimal "s" char* str "y" char* bytes "O" PyObject* 任意 Python 对象 */ PyObject* function1(PyObject* self) { return Py_BuildValue("is", 100, "Hello C API!"); } // 接收任意类型的关键字参数 // 接收任意类型的关键字参数 PyObject* function2(PyObject* self, PyObject* args) { Py_ssize_t size = PyTuple_Size(args); printf("function2 接收到的参数数量为: %lld\n", size); long i1 = 0; double i2 = 0.0; char* i3 = NULL; PyObject* i4 = NULL; for (Py_ssize_t i = 0; i < size; ++i) { PyObject* arg = PyTuple_GetItem(args, i); if (PyLong_Check(arg)) { PyArg_Parse(arg, "l", &i1); printf("第%d个参数为int/long类型,值为:%d\n", i, i1); } if (PyFloat_Check(arg)) { PyArg_Parse(arg, "f", &i2); printf("第%ld个参数为float/double类型,值为:%f\n", i, i2); } if (PyUnicode_Check(arg)) { PyArg_Parse(arg, "s", &i3); printf("第%ld个参数为double类型,值为:%s\n", i, i3); } if (PyList_Check(arg)) { printf("第%ld个参数为 list 类型\n", i); } if (PySet_Check(arg)) { printf("第%ld个参数为 set 类型\n", i); } if (PyDict_Check(arg)) { printf("第%ld个参数为 dict 类型\n", i); } if(PyTuple_Check(arg)) { printf("第%ld个参数为 tuple 类型\n", i); } } return Py_None; } PyObject* function3(PyObject* self, PyObject* args, PyObject* kwargs) { Py_ssize_t tsize = PyTuple_GET_SIZE(args); Py_ssize_t dsize = PyDict_GET_SIZE(kwargs); printf("位置参数个数为:%ld,关键字参数个数为:%ld\n", tsize, dsize); int i1, i2, i3 = 0; char* kwlist[] = {"i1", "i2", "i3", "a", "b", "c", NULL}; int a = 0; char* b = NULL; float c = 0.0; PyArg_ParseTupleAndKeywords(args, kwargs, "iii|isf", kwlist, &i1, &i2, &i3, &a, &b, &c); printf("三个位置类型参数值为:%d %d %d\n", i1, i2, i3); printf("三个关键字类型参数值为:%d %s %f\n", a, b, c); return Py_None; }
2.3 def_module.cpp
#include "my_function.h" #include <Python.h> // 模块初始化函数 PyMODINIT_FUNC PyInit_c_module() { // 将模块函数的函数列表 static PyMethodDef py_methods[] = { // 设置 METH_NOARGS 时,函数无法返回任何值,即使返回也是 None {"function1", (PyCFunction)function1, METH_NOARGS, "function1"}, {"function2", (PyCFunction)function2, METH_VARARGS, "function2"}, {"function3", (PyCFunction)function3, METH_VARARGS|METH_KEYWORDS, "function3"}, {NULL, NULL, 0, NULL} }; // 模块定义 static PyModuleDef py_modules = { PyModuleDef_HEAD_INIT, "c_module", NULL, -1, py_methods }; return PyModule_Create(&py_modules); }
2.4 c_function_wrapper.py
import c_module def function1(): return c_module.function1() def function2(*args): return c_module.function2(*args) def function3(*args, **kwargs): return c_module.function3(*args, **kwargs)
从这里可以看到,为了能够在 Python 较为方便调用 C 函数,我们最好写一个 wrapper 文件,作为 C/CPP 函数包装,当然程序中直接 import 也是可以的。
3. 打包以及测试
打包之前先要编写一些配置信息:
setup.py
from setuptools import setup from setuptools import find_packages # 只能打包包含 __init__.py 文件的包 from setuptools import find_namespace_packages # 只能不包含 __init__.py 的独立模块 from setuptools import Extension import glob setup( # 指定发布包基本信息 name = "my-test", version = "1.6.3", author = "edward meng", author_email = "chinacpp@hotmail.com", description = "python and c", url = "http://mengbaoliang.com/", license = 'Apache License 2.0', # 指定包中的代码 packages=find_namespace_packages() + find_packages(), # 指定运行的 Python 版本,如果使用的版本非指定版本则安装失败 python_requires='>=2.7, <=3.9', # include_package_data 设置为 True,打包时会解析 MANIFEST.in 文件,从而确定还打包那些非 py 的文件 include_package_data=True, # c/cpp 扩展模块 ext_modules = [ Extension( 'c_module', # 模块名要和 # 导出的模块名一致 language='c++', sources=glob.glob('my_test/cpp/c_module/*.cpp'), extra_compile_args = ['-std=c++11'], ) ] )
MANIFEST.in
include README.md include my_test/cpp/c_module/*.h
执行下面的命令,将 cpp 文件编译成 so 的动态库,并生成工具包 my-test:
python setup.py bdist_wheel
此时在 dist 目录下会生成如下文件:
my_test-1.6.3-cp38-cp38-macosx_10_9_x86_64.whl
该文件为编译完成之后的安装文件,但是需要注意的是,该文件是在 Mac 平台打包的,换了平台是无法使用的。你可以打成源码包的形式:
python setup.py sdist
此时,生成的文件如下:
my-test-1.6.3.tar.gz
无论打成二进制,还是源码形式, cd 到 dist 目录执行下面的安装命令:
pip install xxx.tar.gz
创建一个新项目,切换到安装 my-test 包的环境中,输入下面的代码:
import my_test.python as py import my_test.cpp as cpp def call_c_function(): print(cpp.function1()) print('-' * 50) cpp.function2(10, 20, [10, 20, 30], "hello world", {'a': 100, 'b': 30}, (10, 20), {10, 20}) print('-' * 50) cpp.function3(10, 20, 30, 40, "Hello World", 3.14) def call_python_function(): print(py.function1()) print('-' * 50) print(py.function2([10, 20, 30])) print('-' * 50) print(py.function3(10, 20)) if __name__ == '__main__': call_python_function() print('*' * 50) call_c_function()
程序执行结果:
Hello MyTest! -------------------------------------------------- [110, 120, 130] -------------------------------------------------- 30 ************************************************** (100, 'Hello C API!') -------------------------------------------------- function2 接收到的参数数量为: 7 第0个参数为int/long类型,值为:10 第1个参数为int/long类型,值为:20 第2个参数为 list 类型 第3个参数为double类型,值为:hello world 第4个参数为 dict 类型 第5个参数为 tuple 类型 第6个参数为 set 类型 -------------------------------------------------- 位置参数个数为:6,关键字参数个数为:0 三个位置类型参数值为:10 20 30 三个关键字类型参数值为:40 Hello World 3.140000