我们知道 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 函数。
- 编写 Python 部分函数 {#title-0} ============================
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))
这部分很简单,不做过多解释。
- 编写 CPP 部分函数 {#title-1} =========================
在 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 {#title-2}
#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 {#title-3}
#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 {#title-4}
#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 {#title-5}
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 也是可以的。
- 打包以及测试 {#title-6} ====================
打包之前先要编写一些配置信息:
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