为Python编写C++扩展,并用setuptools打包自己的分发包

2024-05-14 11:12

本文主要是介绍为Python编写C++扩展,并用setuptools打包自己的分发包,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 引言
    • 环境
    • 制作源码包
    • C/C++扩展 Hello World:实现一些函数给Python层调用
      • Optional:类型提示支持
    • 在C++扩展中定义Python数据结构实战:用C++扩展实现树状数组
    • 餐后甜点:用C++扩展实现单调递减数组的二分查找函数
      • 支持int数组的二分查找
      • 支持任意可比较对象的数组的二分查找
    • 单测:pytest
    • 参考资料

引言

最近手痒想写Python和C++代码,于是有了这篇精华水blog。这次打算用C++写些数据结构,打包后给Python调用。

项目GitHub传送门

作者:hans774882968以及hans774882968以及hans774882968

本文52pojie:https://www.52pojie.cn/thread-1923720-1-1.html

本文juejin:https://juejin.cn/post/7368319486778703898

本文CSDN:https://blog.csdn.net/hans774882968/article/details/138812416

环境

  • Windows10 VSCode
  • Python 3.7.6 pytest 7.4.4 setuptools 68.0.0
  • g++.exe (x86_64-win32-seh-rev1, Built by MinGW-Builds project) 13.1.0 from here

setuptools官方文档建议舍弃setup.py(setup脚本)的写法,转而使用一个叫build的命令行工具。但该工具只支持到Python3.8,所以本文依旧使用setup.py

制作源码包

顾名思义,源码包就是不在打包阶段进行编译工作,到pip install阶段再执行。根据参考链接1,我们可以用setuptools制作源码包。首先我们新建一个项目,目录如下:

.
│  setup.py 调用 setuptools.setup
├─binary_indexed_treebit.py 最简单的命令行程序__init__.py 留空就行。为了让 setuptools 扫描到这个包,必须要有

setup.py可以参考setup脚本文档传送门来写:

from setuptools import setup, find_packagessetup(name='data-structure-demos-hans',version='0.1',description='provide some data structures for competitive programming',author='<author name>',author_email='<your email>',url='https://github.com/Hans774882968/data-structure-demos-hans',packages=find_packages('.'),license='MIT',
)

find_packages用来查找当前项目有哪些包,文档。find_packages('.')只指定了where='.',表示从当前目录开始扫描,把所有含有__init__.py的目录都当成包。

制作源码包命令:python setup.py sdist

running sdist
running egg_info
creating data_structure_demos_hans.egg-info
writing data_structure_demos_hans.egg-info\PKG-INFO
writing dependency_links to data_structure_demos_hans.egg-info\dependency_links.txt
writing top-level names to data_structure_demos_hans.egg-info\top_level.txt
writing manifest file 'data_structure_demos_hans.egg-info\SOURCES.txt'
reading manifest file 'data_structure_demos_hans.egg-info\SOURCES.txt'
writing manifest file 'data_structure_demos_hans.egg-info\SOURCES.txt'
running check
creating data-structure-demos-hans-0.1
creating data-structure-demos-hans-0.1\binary_indexed_tree
creating data-structure-demos-hans-0.1\data_structure_demos_hans.egg-info
copying files to data-structure-demos-hans-0.1...
copying README.md -> data-structure-demos-hans-0.1
copying setup.py -> data-structure-demos-hans-0.1
copying binary_indexed_tree\__init__.py -> data-structure-demos-hans-0.1\binary_indexed_tree
copying binary_indexed_tree\bit.py -> data-structure-demos-hans-0.1\binary_indexed_tree 
copying data_structure_demos_hans.egg-info\PKG-INFO -> data-structure-demos-hans-0.1\data_structure_demos_hans.egg-info
copying data_structure_demos_hans.egg-info\SOURCES.txt -> data-structure-demos-hans-0.1\data_structure_demos_hans.egg-info
copying data_structure_demos_hans.egg-info\dependency_links.txt -> data-structure-demos-hans-0.1\data_structure_demos_hans.egg-info
copying data_structure_demos_hans.egg-info\top_level.txt -> data-structure-demos-hans-0.1\data_structure_demos_hans.egg-info
Writing data-structure-demos-hans-0.1\setup.cfg
creating dist
Creating tar archive
removing 'data-structure-demos-hans-0.1' (and everything under it)

运行完毕后生成dist目录和data_structure_demos_hans.egg-info目录。后者暂时不用理会,而dist目录下有data-structure-demos-hans-0.1.tar.gz,这就是我们的源码包。

接下来运行pip install data-structure-demos-hans-0.1.tar.gz安装我们刚刚打好的包:

Looking in indexes: https://mirrors.aliyun.com/pypi/simple
Processing <project dir>\dist\data-structure-demos-hans-0.1.tar.gzPreparing metadata (setup.py) ... done
Building wheels for collected packages: data-structure-demos-hansBuilding wheel for data-structure-demos-hans (setup.py) ... doneCreated wheel for data-structure-demos-hans: filename=data_structure_demos_hans-0.1-py3-none-any.whl size=1943 sha256=14aa4f5dff5de02c7d2e6125ef3c0127ec6466a97929573b355545fea9800b3fStored in directory: <%homepath%>\appdata\local\pip\cache\wheels\47\64\58\02429501ec651b154a43b87f2630ad2d34b69df5fb31ee1d49
Successfully built data-structure-demos-hans
Installing collected packages: data-structure-demos-hans
Successfully installed data-structure-demos-hans-0.1

运行pip list可看到这一行data-structure-demos-hans 0.1已经出现。此时进入<python install dir>\Lib\site-packages可以看到binary_indexed_tree文件夹和data_structure_demos_hans-0.1.dist-info文件夹。因此,调用树状数组的代码可以这么写:

from binary_indexed_tree.bit import BITPydef main():a1 = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]b1 = BITPy(len(a1))for i, v in enumerate(a1):b1.add(i + 1, v)for i in range(1, len(a1) + 1):print(b1.sum(i))if __name__ == '__main__':main()

C/C++扩展 Hello World:实现一些函数给Python层调用

新增文件夹cpp_extension_hello

cpp_extension_hellohello.cpp__init__.py 空文件

和上一章类似,我们只需要修改setup.pyhello.cpp。先看setup.py的改动:

from setuptools import setup, find_packages, Extensionextensions = [Extension('cpp_extension_hello.hello_cpp_extension',['cpp_extension_hello/hello.cpp',],language='c++'),
]setup(# omit other paramsext_modules=extensions,
)

Extension类代表一个C/C++模块。这里指定了模块名hello_cpp_extension,那么链接时就会去找PyInit_hello_cpp_extension这个函数,这个函数用于模块初始化。如果打算写C模块,可以省略language='c++'

在写C++代码前,最好先让VSCode能够找到Python.h。这次我们先不用cmake,而是选择一个更轻量的方案:配置.vscode\c_cpp_properties.jsonCtrl+Shift+P选择C/C++ Edit Configurations,会自动生成上述文件。

{"configurations": [{// ..."includePath": ["${workspaceFolder}/**","<python install path>\\include" // Python.h 所在的目录],"intelliSenseMode": "windows-gcc-x64","compilerPath": "<your mingw64 path>\\bin\\g++"}],// ...
}

接下来看hello.cpp

#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <iostream>static PyObject* cpp_extension_hello(PyObject* self, PyObject* args) {const char* name;if (!PyArg_ParseTuple(args, "s", &name)) {return nullptr;}std::cout << "[cpp_extension_hello] hello, " << name << std::endl;Py_RETURN_NONE;
}static PyMethodDef cppExtensionMethods[] = {{"hello_from_cpp_extension", cpp_extension_hello, METH_VARARGS,"Print a hello message"},{NULL, NULL, 0, NULL}  // Sentinel is required
};static struct PyModuleDef cpp_extension_module = {PyModuleDef_HEAD_INIT,"hello_cpp_extension", NULL,-1, cppExtensionMethods};PyMODINIT_FUNC PyInit_hello_cpp_extension() {return PyModule_Create(&cpp_extension_module);
}

注意点:

  1. 这个文件格式有一些要求,我模仿着参考链接3写的。
  2. static struct PyModuleDef cpp_extension_modulem_name应该要和setup.py指定的一致?TODO
  3. cppExtensionMethods的哨兵是必要的,否则运行时会报错module functions cannot set METH_CLASS or METH_STATIC
  4. PyMethodDef.ml_name指定了模块调用者使用的方法名为hello_from_cpp_extension

在此我希望用我本地的g++来编译cpp,所以需要建一个setup.cfg

[build]
compiler=mingw32

如何查看支持的编译器:根据参考链接6,输入以下命令即可。

python setup.py build_ext --help-compiler

输出示例:

List of available compilers:--compiler=bcpp     Borland C++ Compiler--compiler=cygwin   Cygwin port of GNU C Compiler for Win32--compiler=mingw32  Mingw32 port of GNU C Compiler for Win32--compiler=msvc     Microsoft Visual C++--compiler=unix     standard UNIX-style compiler

之后,我们执行python setup.py sdist并不会编译cpp,因为这个编译过程发生在pip install data-structure-demos-hans-0.1.tar.gz阶段。接下来如果不出意外的话,就要出意外了TAT。pip install报错:

        File "<python install dir>\lib\distutils\cygwinccompiler.py", line 86, in get_msvcrraise ValueError("Unknown MS Compiler version %s " % msc_ver)ValueError: Unknown MS Compiler version 1916

幸好,经过一番搜索,找到了参考链接2。按其指示,我们新增一小段代码就OK:

        elif msc_ver == '1916':# manually add because "Unknown MS Compiler version 1916". VS2015return ['msvcr120']

pip install成功情况下的输出如下:

Looking in indexes: https://mirrors.aliyun.com/pypi/simple
Processing <project dir>\dist\data-structure-demos-hans-0.1.tar.gzPreparing metadata (setup.py) ... done
Building wheels for collected packages: data-structure-demos-hansBuilding wheel for data-structure-demos-hans (setup.py) ... doneCreated wheel for data-structure-demos-hans: filename=data_structure_demos_hans-0.1-cp37-cp37m-win_amd64.whl size=23786 sha256=96cfef2a169f3edda75a86d8303fee8724b7b32bcb496328d7161404807cce85Stored in directory: <%homepath%>\appdata\local\pip\cache\wheels\47\64\58\02429501ec651b154a43b87f2630ad2d34b69df5fb31ee1d49
Successfully built data-structure-demos-hans
Installing collected packages: data-structure-demos-hansAttempting uninstall: data-structure-demos-hansFound existing installation: data-structure-demos-hans 0.1Uninstalling data-structure-demos-hans-0.1:Successfully uninstalled data-structure-demos-hans-0.1
Successfully installed data-structure-demos-hans-0.1

安装成功后,可以找到一个pyd文件<python install path>\Lib\site-packages\cpp_extension_hello\hello_cpp_extension.cp37-win_amd64.pyd。最后写段代码调用一下:

from cpp_extension_hello.hello_cpp_extension import hello_from_cpp_extensiondef main():hello_from_cpp_extension('hans')if __name__ == '__main__':main()

Optional:类型提示支持

虽然调用成功了,但是没有类型提示,不太友好。首先我们需要加一个.pyi文件。.pyi文件的名字要和import的文件名一致,所以文件名是hello_cpp_extensionhello_cpp_extension.pyi

def hello_from_cpp_extension(name: str) -> None:pass

接下来我们要把.pyi文件打包进去,但默认情况下它不会被打包进去。根据参考链接4,如果你的setuptools版本大于等于69.0.0,那么可以比较容易地做到这件事。但我的是68.0.0,因此需要:

  1. 传递选项include_package_data=True
  2. 新增MANIFEST.in
recursive-include * *.pyi

之后再打包,就能看到.pyi文件被打包进去了。

Q:不需要加一个空的py.typed文件?A:是的。本项目场景比较简单,暂时不需要理会PEP-561规范。

在C++扩展中定义Python数据结构实战:用C++扩展实现树状数组

binary_indexed_tree目录新增两个文件bit.cppbit_cpp_extension.pyi

binary_indexed_treebit.cppbit.pybit_cpp_extension.pyi__init__.py

setup.py例行更改extensions数组:

extensions = [Extension('binary_indexed_tree.bit_cpp_extension',['binary_indexed_tree/bit.cpp',],language='c++'),# ...
]

在C++扩展中定义一个Python数据结构至少需要定义两个数据结构,类类型数据结构和对象类型数据结构。

对象类型数据结构用来装你需要的数据,但是必须包含一个PyObject_HEAD头部。本章新增的对象类型数据结构如下:

struct BITObject {PyObject_HEAD int n;int* a;
};

类类型数据结构是一个PyTypeObject类型的实例。这家伙有相当多的字段,但在本例中我们需要提供的很少。结合一个叫pysegmenttree的项目(具体参考的代码传送门)和参考链接5,和上面定义的对象类型数据结构,本章新增的类类型数据结构主要需要提供的字段只有tp_dealloctp_flagstp_doctp_methodstp_new = bit_new。其他字段都保持为nullptr即可。

static PyTypeObject BitType = {PyVarObject_HEAD_INIT(NULL, 0) "binary_indexed_tree.bit_cpp_extension.BIT",sizeof(BITObject),.tp_dealloc = (destructor)bit_dealloc,.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,.tp_doc = "Binary Indexed Tree",.tp_methods = bit_methods,.tp_new = bit_new,
};
  1. PyVarObject_HEAD_INIT(NULL, 0)是固定写法。
  2. 根据参考链接5,tp_name = binary_indexed_tree.bit_cpp_extension.BIT不是随意定义的字符串。它应对应Python调用者的import方式from binary_indexed_tree.bit_cpp_extension import BIT
  3. tp_basicsize指定为sizeof(对象类型数据结构)
  4. tp_doc随意写即可。

接下来咱把目光放到模块初始化函数。相比于上一章的一句话return PyModule_Create(&cpp_extension_module);,这次的代码量要大得多了,但也是看着参考链接5和pysegmenttree项目,按格式来即可。

PyMODINIT_FUNC PyInit_bit_cpp_extension() {PyObject* m;if (PyType_Ready(&BitType) < 0) {return nullptr;}m = PyModule_Create(&cpp_extension_module);if (m == nullptr) {return nullptr;}Py_INCREF(&BitType);if (PyModule_AddObject(m, "BIT", (PyObject*)&BitType) < 0) {Py_DECREF(&BitType);Py_DECREF(m);return nullptr;}return m;
}

我们接着看类类型数据结构涉及的几个对象和方法。

bit_methods:我们希望提供的类的成员方法。

static PyMethodDef bit_methods[] = {{"sum", (PyCFunction)bit_sum, METH_VARARGS | METH_KEYWORDS,"Queries prefix sum"},{"add", (PyCFunction)bit_add, METH_VARARGS | METH_KEYWORDS,"Performs the add operation"},{nullptr},  // Sentinel is required
};

bit_sumbit_add的函数签名模仿着参考链接5写就行。bit_sumbit_add和它们共用的入参校验逻辑jdg_idx如下:

static bool jdg_idx(BITObject* self, int idx) {if (idx < 0) {PyErr_SetString(PyExc_ValueError,"idx should be greater than or equal to 0");return false;}if (idx > self->n) {PyErr_SetString(PyExc_ValueError,"idx should be less than or equal to array size");return false;}return true;
}static PyObject* bit_add(BITObject* self, PyObject* args, PyObject* kwds) {int idx = 0, v = 0;if (!PyArg_ParseTuple(args, "ii", &idx, &v)) {return nullptr;}if (!jdg_idx(self, idx)) {return nullptr;}for (; idx <= self->n; idx += idx & -idx) self->a[idx] += v;Py_RETURN_NONE;
}static PyObject* bit_sum(BITObject* self, PyObject* args, PyObject* kwds) {int idx = 0;if (!PyArg_ParseTuple(args, "i", &idx)) {return nullptr;}if (!jdg_idx(self, idx)) {return nullptr;}int res = 0;for (; idx; idx -= idx & -idx) res += self->a[idx];PyObject* res_py = PyLong_FromLong(res);return res_py;
}

这里用到了几个API:

  1. PyArg_ParseTuple用来拿入参。ii表示取两个int。
  2. PyErr_SetString用来抛异常。这里我抛出了ValueError
  3. 因为几个方法返回值都得是PyObject*,所以需要用PyLong_FromLong把C++的int转为PyObject*

抽象出jdg_idx的过程:原本的写法是:

  if (idx < 0) {PyErr_SetString(PyExc_ValueError,"idx should be greater than or equal to 0");return nullptr;}

那么我们可以将其变为PyErr_SetString; return false/true; return nullptr;,于是可以抽象出一个返回bool的函数,然后这么调用:

  if (!jdg_idx(self, idx)) {return nullptr;}

bit_new:进行对象的初始化工作。除了type->tp_alloc(type, 0)是没见过的固定写法外,其他内容我们都比较熟悉了。tp_alloc顾名思义就是给对象类型数据结构分配内存。

static PyObject* bit_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {BITObject* self = (BITObject*)type->tp_alloc(type, 0);int n = 0;if (!PyArg_ParseTuple(args, "i", &n)) {Py_DECREF(self);return nullptr;}self->n = n;self->a = new int[n + 1]();return (PyObject*)self;
}

bit_dealloc:进行堆内存释放工作,依据参考链接5,不写也能跑,但会造成内存泄露。

static void bit_dealloc(BITObject* self) {delete[] self->a;Py_TYPE(self)->tp_free(self);
}

即使我们不提供该函数,Python解释器也会调用Py_TYPE(self)->tp_free(self)。但我们提供该函数后,就不得不手动加上这句话了。

最后按惯例,展示一下我们刚刚实现的树状数组的用法:

from binary_indexed_tree.bit_cpp_extension import BITdef bit_cpp(a: List[int]):b1 = BIT(len(a))for i, v in enumerate(a):b1.add(i + 1, v)for i in range(1, len(a) + 1):print(b1.sum(i))b1.add(9, 113964)print(b1.sum(len(a)))b2 = BIT(20)try:b2.add(-2, 10)except ValueError as e:print(e)try:b2.sum(-1)except ValueError as e:print(e)try:b2.sum(22)except ValueError as e:print(e)try:b2.add(21, 10)except ValueError as e:print(e)b2.add(2, 20)b2.add(4, 30)print(b2.sum(5))  # 50

餐后甜点:用C++扩展实现单调递减数组的二分查找函数

本节我们模仿bisect包的API,用C++扩展实现单调递减数组的二分查找函数。

支持int数组的二分查找

新增模块bisect_decr_cpp,提供的方法如下:

const char* lower_bound_intro ="The return value i is such that all e in a[:i] have e > x, and all e in ""a[i:] have e <= x";
const char* upper_bound_intro ="The return value i is such that all e in a[:i] have e >= x, and all e in ""a[i:] have e < x";static PyMethodDef cppExtensionMethods[] = {{"dec_lower_bound", dec_lower_bound, METH_VARARGS, lower_bound_intro},{"dec_upper_bound", dec_upper_bound, METH_VARARGS, upper_bound_intro},{NULL, NULL, 0, NULL}  // Sentinel is required
};

dec_lower_bounddec_upper_bound几乎完全一致,只有要用到的比较运算符不一样,所以我抽象出了dec_binary_search方法,并用op区分是哪个方法,op == Py_GT的为dec_lower_boundop == Py_GE的为dec_upper_bound。这里选用Python.h提供的Py_GT, Py_GE宏是在给下一节实现任意可比较对象的数组的二分查找做铺垫。

static bool jdg_arr(PyObject* a_py) {if (!PyList_Check(a_py)) {PyErr_SetString(PyExc_ValueError,"Plz pass an integer array and an integer");return false;}return true;
}static PyObject* dec_binary_search(PyObject* self, PyObject* args, int op) {PyObject* a_py = nullptr;int x = 0;if (!PyArg_ParseTuple(args, "Oi", &a_py, &x)) {PyErr_SetString(PyExc_ValueError,"Plz pass an integer array and an integer");return nullptr;}if (!jdg_arr(a_py)) {return nullptr;}int n = PyList_GET_SIZE(a_py);int l = 0, r = n;while (l < r) {int mid = (l + r) >> 1;PyObject* num_py = PyList_GET_ITEM(a_py, mid);if (!PyLong_Check(num_py)) {PyErr_SetString(PyExc_ValueError,"Every elements of the array should be integer");return nullptr;}int num = PyLong_AS_LONG(num_py);bool cmp_res = false;if (op == Py_GT) {cmp_res = num > x;} else {cmp_res = num >= x;}if (cmp_res) {l = mid + 1;} else {r = mid;}}PyObject* res_py = PyLong_FromLong(l);return res_py;
}static PyObject* dec_lower_bound(PyObject* self, PyObject* args) {return dec_binary_search(self, args, Py_GT);
}static PyObject* dec_upper_bound(PyObject* self, PyObject* args) {return dec_binary_search(self, args, Py_GE);
}

在此我们看到一个具有普适性的规律:C++代码和Python代码属于两个抽象层,因此我们的工作流程一般如下:先把Python抽象层的语言翻译为C++抽象层的语言,接着判断参数的合法性,然后做些计算,最后再翻译回Python抽象层的语言。

  1. 抽象出jdg_arr的思路同上一节。
  2. PyList_Check, PyLong_Check用于校验参数类型,PyList_GET_SIZE拿列表长度,PyList_GET_ITEM根据下标拿列表元素。
  3. 为了保证复杂度为O(logn),不能遍历整个数组,于是检验单个元素的类型的逻辑不得不和二分查找的逻辑耦合在一起。

最后按惯例,展示一下其用法:

from bisect_decr.bisect_decr_cpp import dec_lower_bound, dec_upper_bounddef case1():a = [100, 90, 80, 70, 60, 50, 40, 30, 20, 10]l1 = dec_lower_bound(a, 101)l2 = dec_lower_bound(a, 61)l3 = dec_lower_bound(a, 9)l4 = dec_lower_bound(a, 100)l5 = dec_lower_bound(a, 60)l6 = dec_lower_bound(a, 10)u1 = dec_upper_bound(a, 101)u2 = dec_upper_bound(a, 61)u3 = dec_upper_bound(a, 9)u4 = dec_upper_bound(a, 100)u5 = dec_upper_bound(a, 60)u6 = dec_upper_bound(a, 10)print(l1, l2, l3, l4, l5, l6)  # 0 4 10 0 4 9print(u1, u2, u3, u4, u5, u6)  # 0 4 10 1 5 10def case2():a = [90, 90, 90, 80, 80, 70]l1 = dec_lower_bound(a, 91)l2 = dec_lower_bound(a, 90)l3 = dec_lower_bound(a, 85)l4 = dec_lower_bound(a, 80)l5 = dec_lower_bound(a, 75)l6 = dec_lower_bound(a, 70)l7 = dec_upper_bound(a, 60)u1 = dec_upper_bound(a, 91)u2 = dec_upper_bound(a, 90)u3 = dec_upper_bound(a, 85)u4 = dec_upper_bound(a, 80)u5 = dec_upper_bound(a, 75)u6 = dec_upper_bound(a, 70)u7 = dec_upper_bound(a, 60)print(l1, l2, l3, l4, l5, l6, l7)  # 0 0 3 3 5 5 6print(u1, u2, u3, u4, u5, u6, u7)  # 0 3 3 5 5 6 6def main():case1()case2()if __name__ == '__main__':main()

支持任意可比较对象的数组的二分查找

首先,原本的int x = 0参数要改为PyObject*

  PyObject* a_py = nullptr;PyObject* x = nullptr;if (!PyArg_ParseTuple(args, "OO", &a_py, &x)) {PyErr_SetString(PyExc_ValueError,"Plz pass a comparable object array and a comparable object");return nullptr;}// ...

接着微调参数校验逻辑,不是很重要,在此不赘述。然后我们来看本章的关键API:int PyObject_RichCompareBool(PyObject *, PyObject *, int)。第三个参数是运算符,Py_GT, Py_GE分别表示>, >=。返回值为1, 0, -1,0和1就相当于bool结果,-1表示由对象没有重载相关运算符等原因导致比较失败。如果不处理-1这个返回值,会导致Python解释器抛出SystemError,取e.__cause__可以拿到TypeError,比如:TypeError: '>' not supported between instances of 'Person' and 'Person'。我的代码处理了这种情况,因此仍然可以返回ValueError,比如:ValueError: Comparison error. Index: 0. Operator: ">"

改动到的核心代码:

  auto get_cmp_error_info = [op](int idx) {string s = "Comparison error. Index: ";s += to_string(idx);s += ". Operator: \"";if (op == Py_GT)s += ">";elses += ">=";s += "\"";return s;};int n = PyList_GET_SIZE(a_py);int l = 0, r = n;while (l < r) {int mid = (l + r) >> 1;PyObject* num_py = PyList_GET_ITEM(a_py, mid);int cmp_res = PyObject_RichCompareBool(num_py, x, op);if (cmp_res == -1) {string cmp_err_info = get_cmp_error_info(mid);PyErr_SetString(PyExc_ValueError, cmp_err_info.c_str());return nullptr;}if (cmp_res) {l = mid + 1;} else {r = mid;}}

使用:

from bisect_decr.bisect_decr_cpp import dec_lower_bound, dec_upper_bounddef test_comparable_object_array():class Person():def __init__(self, age: int) -> None:self.age = agedef __ge__(self, other):return self.age >= other.agedef __gt__(self, other):return self.age > other.agepersons = [Person(60), Person(25), Person(18), Person(18), Person(6)]test_arr = [Person(100), Person(60), Person(33), Person(25), Person(23), Person(18), Person(12), Person(6), Person(4)]res_l = [dec_lower_bound(persons, v) for v in test_arr]assert res_l == [0, 0, 1, 1, 2, 2, 4, 4, 5]

单测:pytest

pip install -U pytest
pip install -U pytest-html
# 测试安装是否成功
pytest --version

常规的断言使用assert关键字就行。异常断言的写法:with pytest.raises(ValueError) as e_info: # write code here that might throw an exception。举例:

    b2 = BIT(20)with pytest.raises(ValueError) as e_info:b2.add(-2, 10)assert 'idx should be greater than or equal to 0' in str(e_info.value)

运行单测命令:

pytest --html=coverage/report.html

参考资料

  1. Python使用setuptools打包自己的分发包并使用举例:https://blog.csdn.net/hxxjxw/article/details/124298543
  2. Windows10下用mingw编译python扩展库:https://blog.51cto.com/lang13002/6723721
  3. https://codedamn.com/news/python/implementing-custom-python-c-extensions-step-by-step-guide
  4. Include type information by default (*.pyi, py.typed):https://github.com/pypa/setuptools/issues/3136
  5. 使用c/c++编写python扩展(三):自定义Python内置类型:https://zhuanlan.zhihu.com/p/106773873
  6. https://setuptools.pypa.io/en/latest/deprecated/distutils/configfile.html

这篇关于为Python编写C++扩展,并用setuptools打包自己的分发包的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/988587

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Python调用Orator ORM进行数据库操作

《Python调用OratorORM进行数据库操作》OratorORM是一个功能丰富且灵活的PythonORM库,旨在简化数据库操作,它支持多种数据库并提供了简洁且直观的API,下面我们就... 目录Orator ORM 主要特点安装使用示例总结Orator ORM 是一个功能丰富且灵活的 python O

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import