给 Python 构建 C++ 扩展模块

我们知道 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 不分,我们有两部分:

  1. my_function.h 和 my_function.cpp 用于定义 cpp 函数
  2. def_module.cpp 定义了根据前面那俩 cpp 文件生成模块的信息。
  3. 上面的 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
未经允许不得转载:一亩三分地 » 给 Python 构建 C++ 扩展模块
评论 (0)

7 + 1 =