用 C/C++ 扩展 Python 能力

用 C/C++ 扩展 Python 能力

让 Python 跑得快,除了用 ctypes 把现成的 C 动态库包进来,更彻底的做法是用 Python/C API 直接编写扩展模块。Python 解释器本体就是用 C 实现的,按约定实现一个 C 函数、把它注册给解释器,性能关键路径就可以彻底脱离 Python 的动态分发。我们从编写一个能返回值的扩展函数讲起,接着处理接收各种参数的函数,最后实现一个 Python 与 C++ 混合的完整工具包并打包分发。三章代码覆盖 C、C++、Python 三端,按顺序读完即可自己动手写扩展。

1. 编写扩展函数

1.1 编写 C 函数

libtest.cpp 程序文件如下:

#include <string>
#include <iostream>
#include <stdlib.h>
#include "Python.h"
#include "structmember.h"


// 1. 返回 None
extern "C" PyObject* py_test_function1(PyObject *, PyObject *)
{
    Py_RETURN_NONE;
}


// 2. 返回整数类型
extern "C" PyObject* py_test_function2(PyObject *, PyObject *)
{
    return Py_BuildValue("i", 100);
}


// 3. 返回元组类型
extern "C" PyObject* py_test_function3(PyObject *, PyObject *)
{
    // 创建包含3个元素的元组
    PyObject* tuple = PyTuple_New(3);
    PyTuple_SetItem(tuple, 0, Py_BuildValue("i", 10));
    PyTuple_SetItem(tuple, 1, Py_BuildValue("i", 20));
    PyTuple_SetItem(tuple, 2, Py_BuildValue("i", 30));

    // return tuple;
    return Py_BuildValue("iii", 10, 20, 30);
    // 等价于 (10, 20, 30)
    // Py_BuildValue("((ii)(ii)) (ii)", 10, 20, 30, 40, 50, 60)
    // 等价于 ((10, 20), (30, 40)), (50, 60))

    // Py_BuildValue("[i,i]", 10, 20)
    // 等价于 [10, 20]

    // Py_BuildValue("ss", "aaa", "bbb")
    // 等价于 ('aaa', 'bbb')

    // Py_BuildValue("s#", "abcde", 4)
    // 等价于 'abcd'

    // Py_BuildValue("")
    // 等价于 None
}


// 4. 返回字典
extern "C" PyObject* py_test_function4(PyObject *, PyObject *)
{
    // 创建包含3个元素的元组
    PyObject* dict = PyDict_New();
    PyDict_SetItemString(dict, "Name", Py_BuildValue("s", "张三"));
    PyDict_SetItemString(dict, "Age", Py_BuildValue("i", 18));
    PyDict_SetItemString(dict, "Gender", Py_BuildValue("s", "男"));

    // return dict;
    return Py_BuildValue("{s:i,s:i,s:i}", "Name", "张三", "Age", 18, "Gender", "男");
    // 等价于 {'Name': '张三', 'Age': 18, 'Gender': '男'}
}


// 定义导出模块信息
PyMODINIT_FUNC PyInit_libtest()
{
    static PyMethodDef py_methods[] =
    {
            {"test_func1", py_test_function1, METH_VARARGS, "测试函数"},
            {"test_func2", py_test_function2, METH_VARARGS, "测试函数"},
            {"test_func3", py_test_function3, METH_VARARGS, "测试函数"},
            {"test_func4", py_test_function4, METH_VARARGS, "测试函数"},
            {NULL, NULL, NULL, NULL}
    };

    static PyModuleDef py_modules =
    {
            PyModuleDef_HEAD_INIT,
            "libtest",
            NULL,
            -1,
            py_methods
    };

    return PyModule_Create(&py_modules);
}

上面的函数是我们最终要导出到 Python 中使用的函数。由于需要用到 Python/C 的一些接口,所以要导入两个头文件:

#include "Python.h"
#include "structmember.h"

由于 C++ 支持函数重载,编译之后的函数名会进行修饰,和 C 语言函数名的修饰方式不同,所以需要加上 extern “C”,使得 C++ 函数按照 C 的方式修饰。

PyObject* 表示函数的返回类型,此时如果有返回值,可以按照下面的方式返回:
  1. 如果函数没有返回,可以使用宏 Py_RETURN_NONE 代替,表示函数返回 None。
  2. 如果函数返回其他值,可以使用 Py_BuildValue() 来构造返回值。

Py_BuildValue 第一个参数指定返回的类型,其他的类型如下:

Python 类型C/C++ 类型
sstrchar*
zstr/Nonechar*/NULL
iintint
llonglong
cstrchar
dfloatdouble
DcomplexPy_Complex
OanyPyObject*
SstrPyStringObject

接下来,我们得编写另外一个函数,来导出我们的 C/C++ 函数,代码如下:

PyMODINIT_FUNC PyInit_libtest()
{
    static PyMethodDef py_methods[] =
    {
            {"test_func1", py_test_function1, METH_VARARGS, "测试函数"},
            {"test_func2", py_test_function2, METH_VARARGS, "测试函数"},
            {"test_func3", py_test_function3, METH_VARARGS, "测试函数"},
            {"test_func4", py_test_function4, METH_VARARGS, "测试函数"},
            {NULL, NULL, NULL, NULL}
    };

    static PyModuleDef py_modules =
    {
            PyModuleDef_HEAD_INIT,
            "libtest",
            NULL,
            -1,
            py_methods
    };

    return PyModule_Create(&py_modules);
}

1.2 setup.py 配置

#setup.py
from distutils.core import setup, Extension


module_name = 'libtest'
setup(name=module_name,ext_modules=[
    Extension(
        module_name,
        sources=['libtest.cpp'],
        extra_compile_args=['-Wall', '-g'],
        include_dirs=['/Library/Frameworks/Python.framework/Versions/3.8/include/python3.8/']
    )])

1.3 编译安装调用

python setup.py build 
python setup.py install

接下来,我们在 Python 中使用该模块,代码如下:

test.py

import libtest


print(libtest.test_func1())
print(libtest.test_func2())
print(libtest.test_func3())
print(libtest.test_func4())

程序输出结果:

None
100
(10, 20, 30)
{'Name': '张三', 'Age': 18, 'Gender': '男'}

2. 接收参数

2.1 为什么写

和 ctypes 比,手写 CPython 扩展麻烦在要写样板代码:函数签名固定是 PyObject* func(PyObject* self, PyObject* args),还要维护 PyMethodDef 表和 PyInit 入口,再用 setup.py 编译。好处是彻底:函数可以拿到任意 Python 对象的指针,直接调 PyList_Append、PyObject_CallMethod 这类 API,性能和内置模块一样,类型转换在 C 侧完成,没有 ctypes 那种段错误风险。

核心的参数解析靠两个函数:PyArg_ParseTuple 接位置参数,PyArg_ParseTupleAndKeywords 接位置加关键字参数。它们都接受一个格式字符串,用一个字母代表一个参数类型,后面跟上 C 变量的地址,解析成功就把值写进这些地址。格式字符串怎么写,是这一节要讲的重点。

2.2 位置参数

先看最简单的一种:接收一个整数和一个字符串。函数签名用 extern “C” 包起来,是因为这是 C++ 文件,要让编译器按 C 的方式修饰符号,Python 解释器才能按名字找到它。函数体里先声明 int a 和 char* b,再调 PyArg_ParseTuple:

// 位置参数: 接收整数和字符串
extern "C" PyObject* py_test_function1(PyObject *self, PyObject *args)
{
    int a = 0;
    char *b = NULL;
    // 接受并将位置参数拷贝到指定的缓冲区中
    // PyArg_ParseTuple 会为字符串开辟缓冲器,不需要自己开辟空间
    PyArg_ParseTuple(args, "is", &a, &b);

    printf("a = %d, b = %s\n", a, b);

    Py_RETURN_NONE;
}

格式字符串 “is” 就是这个函数的契约:i 对应一个 Python 整数,解析时转成 C int,写到 &a,s 对应一个 Python 字符串,Python 内部把它转成 UTF-8 字节数组,指针写到 &b。要注意 b 指向的是 Python 内部缓冲区,函数返回之前一直有效,函数返回之后不能再用,想长期保存要自己 strdup 一份。代码末尾 Py_RETURN_NONE 是个宏,等价于返回一个 None 对象,引用计数加一,这是 C 扩展函数的标准收尾。

常用格式字符对照如下,写参数解析表的时候基本靠这张:

格式字符Python 侧传入C 侧写出的类型
i整数int
l整数long
d浮点数double
s字符串const char*
O任意对象PyObject*

2.3 接收序列

如果 C 侧想拿到整个 Python 列表或元组自己遍历,就用格式字符 O,它不做类型转换,直接把 PyObject* 给你。下面这个函数接收一个序列,先问它多长,再把它转成可快速下标的形式,逐个元素取出来转成 long 打印:

// 位置参数: 接收序列参数
extern "C" PyObject* py_test_function2(PyObject *self, PyObject *args)
{
    PyObject *obj = NULL;
    void *b = NULL;

    // 接收传递来的参数
    PyArg_ParseTuple(args, "O", &obj);

    // 获得元素长度
    int len = PySequence_Size(obj);
    printf("sequece length: %d\n", len);

    // 将参数转换为序列
    PyObject* seq = PySequence_Fast(obj, "expect a sequece");

    // 遍历序列元素
    for (int i=0; i<len; ++i)
    {
        // 从序列中取出元素
        PyObject* item = PySequence_Fast_GET_ITEM(seq, i);

        // 转换数据类型
        long v = PyLong_AsLong(item);
        printf("%ld\n", v);
    }

    Py_RETURN_NONE;
}

几个 API 各管一段。PySequence_Size 像 Python 里的 len(),对列表、元组、任何实现了序列协议的对象都返回长度。PySequence_Fast 把对象转成适合快速下标的形式,传的第二个参数是错误信息,如果传进来的不是序列,它返回 NULL 并把这个消息抛给 Python。转好之后用 PySequence_Fast_GET_ITEM(seq, i) 取下标 i 的元素,这个宏不做错误检查,快但要自己保证下标合法。最后 PyLong_AsLong 把每个元素转成 C long。

2.4 关键字参数

位置参数够用,但 Python 调用方经常写关键字参数,比如 func(a=10, b=20)。要支持这种写法,函数签名要多一个 PyObject* kwargs 参数,解析函数换成 PyArg_ParseTupleAndKeywords,方法标志位加上 METH_KEYWORDS。下面这个函数接收 5 个整数,前两个必填,后三个可选:

// 关键字参数
extern "C" PyObject* py_test_function3(PyObject *self, PyObject *args, PyObject *kwargs)
{
    int a = NULL;
    int b = NULL;
    int c = NULL;
    int d = NULL;
    int e = NULL;

    static char* kwlist[] = {"a", "b", "c", "d", "e", NULL};

    // 接下传入的位置参数
    PyArg_ParseTupleAndKeywords(args, kwargs, "ii|iii", kwlist, &a, &b, &c, &d, &e);

    printf("%d\n", a);
    printf("%d\n", b);
    printf("%d\n", c);
    printf("%d\n", d);
    printf("%d\n", e);

    Py_RETURN_NONE;
}

格式字符串 “ii|iii” 里的竖线是分隔符,竖线前面 a、b 两个是必填,调用方不传就报错,竖线后面 c、d、e 三个是可选,不传就保持调用前的初值。kwlist 是一个字符串数组,按顺序给出每个参数的关键字名字,最后必须用 NULL 收尾,Python 靠这张表把 kwargs 字典里的 “a”、”b” 对应到 &a、&b。这段代码里把 int 初始化成 NULL 是原文的写法,能编译但不规范,规范写法应该写成 0,原因后面误区一节讲。

2.5 模块装配

三个函数写完了,Python 还不知道它们的存在,必须用一张方法表把函数名和 C 函数指针绑在一起,再写一个模块初始化函数。这部分是固定套路,几乎每个 C 扩展都长这样:

// 定义导出模块信息
PyMODINIT_FUNC PyInit_mytest()
{
    static PyMethodDef py_methods[] =
    {
            {"test_func1", py_test_function1, METH_VARARGS, "测试函数"},
            {"test_func2", py_test_function2, METH_VARARGS, "测试函数"},
            {"test_func3", (PyCFunction)py_test_function3, METH_VARARGS | METH_KEYWORDS, "测试函数"},
            {NULL, NULL, 0, NULL}
    };

    static PyModuleDef py_modules =
    {
            PyModuleDef_HEAD_INIT,
            MODULE_NAME,
            NULL,
            -1,
            py_methods
    };

    return PyModule_Create(&py_modules);
}

逐行看。PyInit_mytest 这个名字不能乱起,import mytest 的时候解释器会按这个名字找入口,PyMODINIT_FUNC 是跨平台的返回类型宏。PyMethodDef 表里每一行是一个导出函数:第一个字符串是 Python 里看到的名字,第二个是 C 函数指针,第三个是方法标志,最后一行 {NULL, NULL, 0, NULL} 是表结束标记,漏了会内存越界。

方法标志决定函数签名长什么样,对照如下:

标志位函数签名用途
METH_VARARGS(self, args)只接位置参数
METH_KEYWORDS(self, args, kwargs)接位置加关键字
两者按位或(self, args, kwargs)test_func3 这种

test_func3 因为函数签名是三参数版本,C 类型 PyCFunction 只认两参数版本,所以要强转 (PyCFunction),这是官方文档认可的写法。PyModuleDef 里 MODULE_NAME 是模块名 mytest,-1 表示模块状态不随解释器分块共享,py_methods 指向方法表,最后 PyModule_Create 建出模块对象返回给解释器。

2.6 编译运行

装配代码写完,还要写一个 setup.py 告诉编译工具源文件在哪、Python.h 在哪。原文用的是 Python 3.8 自带的 distutils,include 目录指向框架路径下的 python3.8:

#setup.py
from distutils.core import setup, Extension

# 模块名
MODULE_NAME = 'mytest'

setup(name=MODULE_NAME,
      ext_modules=[
            Extension(
                MODULE_NAME,
                sources=['libtest2.cpp'],
                extra_compile_args=['-Wall', '-g'],
                include_dirs=['/Library/Frameworks/Python.framework/Versions/3.8/include/python3.8/']
            )]
      )

在源码目录跑 python3 setup.py build_ext –inplace 就能在当前目录编译出 mytest.so(macOS 上是 mytest.cpython-38-darwin.so)。要提醒一句,distutils 从 Python 3.10 起被官方废弃,新项目建议换成 setuptools,写法几乎一样,把 from distutils.core 改成 from setuptools 即可。原文的 include 路径是 macOS 官方 Python 框架的典型位置,换台机器或换个 Python 版本,这个路径要跟着改,最稳的办法是用 python3-config –includes 命令查出实际路径。

编译产出的 mytest 模块和纯 Python 模块用法一样,直接 import 进来调:

from mytest import *


test_func1(10, "abc")
print('-' * 30)
test_func2((10, 20, 30))
print('-' * 30)
test_func3(a=10, b=20, c=100, d=200, e=300)

运行输出:

a = 10, b = abc
------------------------------
sequece length: 3
10
20
30
------------------------------
10
20
100
200
300

第一组输出对应 test_func1,i 拿到 10,s 拿到 abc,printf 直接打到 stdout。第二组对应 test_func2,传入的是 3 元素元组,PySequence_Size 返回 3,循环里把 10、20、30 逐个打出来。第三组对应 test_func3,5 个关键字全部传齐,竖线前后的 5 个 int 按顺序打印 10、20、100、200、300。

2.7 常见误区

第一个坑,不检查 PyArg_ParseTuple 的返回值。它返回 -1 表示解析失败,这时候应该 return NULL 把异常抛回 Python。原文三个函数都没写这个判断,传错参数时不会报 TypeError,而是继续用未初始化的变量,行为未定义。规范写法是判断 PyArg_ParseTuple 的返回值,失败就 return NULL。

第二个坑,把 int 初始化成 NULL。NULL 在 C 里是空指针常量,定义成 ((void*)0),给 int 赋值虽然能编译,但语义上是错的,读代码的人会以为这是个指针。可选参数的初值应该写 0,再靠竖线后的格式字符决定是不是覆盖。

第三个坑,PySequence_Fast 返回值没判空。如果调用方传进来的不是序列,比如传个整数,seq 是 NULL,下一行 PySequence_Fast_GET_ITEM 立刻段错误。规范写法是判断 seq 是否为 NULL,不是才继续取下标。

第四个坑,s 拿到的字符串指针存到函数外面。这个指针指向 Python 内部缓冲区,函数返回后下一次垃圾回收就可能失效,要长期保存必须 PyUnicode_FromString 转成新对象,或者 strdup 到自己 malloc 的内存里。

3. 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 函数。

3.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))

这部分很简单,不做过多解释。

3.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 函数。

3.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

3.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;
}

3.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);
}

3.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.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 API 写扩展,核心就三件事:函数签名、参数解析、模块注册。函数统一写成 PyObject* func(PyObject* self, PyObject* args) 的形式,返回值用 Py_BuildValue 或 PyLong_FromLong 这类构造器生成,参数用 PyArg_ParseTuple / PyArg_ParseTupleAndKeywords 按格式字符串拆解,i、s、O 等格式符覆盖整数、字符串和任意对象。模块级联靠 PyMethodDef 表、PyModuleDef 结构和 PyInit 入口函数,最后用 setup.py 编译安装。再往上走一步,把 Python 包装层和 C++ 实现放进同一个包,用 setuptools 的 Extension 编译、bdist_wheel 或 sdist 打包,就能产出 pip install 即用的混合工具包。写扩展时最容易翻车的是内存所有权:解析参数失败记得返回 NULL 并设置异常,函数返回的字符串不要指向函数内部的临时缓冲区,Python 对象的引用计数要按规则管理。这些检查到位,扩展才稳。掌握了这套 API,需要速度的模块都可以用 C/C++ 重写并接回 Python。