面向-Windows-程序员的-C---软件互操作教程-二-

118 阅读28分钟

面向 Windows 程序员的 C++ 软件互操作教程(二)

原文:C++ Software Interoperability for Windows Programmers

协议:CC BY-NC-SA 4.0

七、构建 Python 扩展模块

介绍

在这一章和下一章中,我们将着眼于构建 Python 扩展模块。这些是将 C/C++ 连接到 Python 的组件。Python 已经存在很长时间了,多年来,已经开发了许多不同的方法来实现这一点。表 7-1 列出了一些方法。

表 7-1

将 C/C++ 连接到 Python 的方法 1

|

方法

|

过时的

|

代表性用户

| | --- | --- | --- | | 【CPython 的 C/C++ 扩展模块 | One thousand nine hundred and ninety-one | 标准程序库 | | PyBind11(推荐用于 C++) | Two thousand and fifteen |   | | Cython(建议用于 C) | Two thousand and seven | 盖文,基维 | | 持有期收益率 | Two thousand and nineteen |   | | mypyc | Two thousand and seventeen |   | | ctypes(类型) | Two thousand and three | oscrypto | | 财务信息管理系统 | Two thousand and thirteen | 密码学,pypy | | 大喝 | One thousand nine hundred and ninety-six | 你们这些混蛋 | | 助推。Python | Two thousand and two |   | | cppyy | Two thousand and seventeen |   |

在本章和接下来的章节中,我们将重点介绍三种主要的方法。这些在表 7-1 中突出显示。在本章中,我们从一个使用 CPython 的“原始”Python 项目开始。这是有教育意义的。我们将看到如何从头开始建立一个 Python 扩展模块项目,以及如何从我们的小型统计库中公开功能。这让我们有机会了解模块是如何定义的,以及PyObject是如何在转换层中使用的。它还说明了低级方法的一些困难。第章第 8 关注助推。Python 然后是 PyBind。这两个框架都提供了有用的工具,克服了我们在编写 CPython 扩展模块时面临的一些问题。我们还将研究如何公开类和函数。最后,我们使用我们构建的模块来说明检查对象和测量性能以及其他事情。

先决条件

对于本章和下一章,主要的先决条件是 Python 安装( www.python.org/downloads/ )。对于这本书,我们使用 Python 3.8(这个项目开始时可用的最新版本)。除了 Python 的版本,我们还需要了解构建环境。在下一章中,我们将需要 Boost。Python 和 Boost。Python 库需要针对这个相同版本的 Python 来构建。

使用 Visual Studio 社区版 2019

在同一个解决方案中,使用 Visual Studio 管理 C++ 项目(我们这样做)和 Python 项目(我们不这样做)是完全可能的。这样做的好处是,您可以在开发 C++ 组件的同一环境中调试 Python 脚本。然而,这种设置有一个缺点。它将我们与 Visual Studio Community Edition 2019 针对的 Python 版本(目前为 Python 3.7)联系起来。而这反过来又会导致 Python、Boost 版本的不一致。Python 和 C++ 项目。为了开发 Python 模块,我们确实需要将 Python (3.8)版本和 Boost 库的 Boost 发行版结合起来。Python(使用 Python 3.8 构建)。

因此,这里的建议是将两个开发领域分开。我们对 C++ 包装器组件使用 Visual Studio Community Edition 2019,对 Python 项目和脚本使用 VSCode。这意味着我们可以方便地使用 MSBuild 编译扩展模块,而不必编写自己的安装和构建脚本。这种方法的优点是使调试稍微容易一些,尽管不如使用完全混合模式调试那样无缝。

StatsPythonRaw

我们的第一个扩展模块是一个名为 StatsPythonRaw 的“原始”Python 项目。我们首先看看项目设置,然后看看代码是如何组织的。在此过程中,我们将研究如何公开底层统计库的函数以及类型转换层。我们还处理异常处理。在最后一节中,我们将练习 Python 客户机的功能,并研究如何调试扩展模块。

项目设置

StatsPythonRaw 是作为 Windows 动态链接库(DLL)项目创建的。该项目引用 StatsLib 静态库。表 7-2 总结了项目设置。

表 7-2

StatsPythonRaw 的项目设置

|

标签

|

财产

|

价值

| | --- | --- | --- | | 一般 | C++ 语言标准 | ISO C++17 标准版(/std:c++17) | | 先进的 | 目标文件扩展名 | 。pyd | | C/C++ >常规 | 其他包含目录 | <用户\用户>\蟒蛇 3 \包含*$(解决方案目录)通用\包含* | | 链接器>常规 | 附加库目录 | <用户\用户> \Anaconda3\libs | | 生成事件>后期生成事件 | 命令行 | 请参见下文 |

以下几点值得注意。首先,我们将目标输出从 dll 改为 pyd 。这表明输出是一个 Python 扩展库。其次,我们需要注意 Python 安装的位置。在附加的包含目录中,我们引用了可以找到 Python.h\include 目录。在附加的库目录中,我们引用了 \libs 目录(而不是 \Lib\Library ,它们也存在于 Python 发行版中)。这就是可以找到 python38.lib 的地方。最后,我们将statspithonraw . pyd模块复制到 Python 脚本( *)所在的目录中。py )导入它所在的位置。我们在后期构建步骤中使用以下脚本:

del "$(SolutionDir)StatsPython\$(TargetName).pyd"
copy /Y "$(OutDir)$(TargetName)$(TargetExt)" "$(SolutionDir)StatsPython\$(TargetName)$(TargetExt)"

这简化了 Python 的设置。通过将 pyd 文件复制到脚本将要执行的位置,我们避免了必须调用 setup.py 来将 Python 模块安装到 Python 环境中。在生产场景中,这是必需的。然而,为了便于说明,我们走这条捷径。

代码组织

在 StatsPythonRaw 项目下,代码被组织成三个主要区域:我们想要公开的函数(functions . h/functions . CPP)、转换层(conversion . h/conversion . CPP)以及我们正在构建的扩展模块( module.cpp )。我们将依次处理这些问题。

功能

声明

在文件 Functions.h 中,我们声明了用于公开底层功能的包装函数。为了方便起见,我们将所有函数放在一个名为API的名称空间中。清单 7-1 再现了完整的声明。

-

Listing 7-1Declaration of the wrapper functions we want to expose

img/519150_1_En_7_Figa_HTML.png

在清单 7-1 中,文件顶部的预处理宏很重要。Python 文档(参见附加参考资料部分)建议我们在包含任何标准库头文件之前使用#define PY_SSIZE_T_CLEAN。例如,当使用有大小的对象、列表和数组时,宏指的是 size 变量的类型。如果定义了宏,那么大小类型是Py_ssize_t,否则大小类型是int。接下来,我们有一些构建指令。我们希望能够构建这个扩展模块的调试和发布版本。但是,我们不想链接调试版本的 Python 库,因为我们还没有安装它们。如果没有这个预处理器指令,当我们构建 StatsPythonRaw 的调试版本时,链接器将试图链接到 python38_d.lib 。因为我们没有这个,所以会产生一个构建错误。所以,我们被要求UNDEF对准_DEBUG符号。如果您下载并安装 Python 调试库,您可以删除它。最后,#include <Python.h>引入 Python API。

API名称空间中,所有的 C++ 包装函数都返回一个PyObject指针。这可以被认为是一个不透明的类型或句柄。通常,Python 运行时将参数作为PyObject传递给我们,然后我们需要解释这些参数。当我们从函数返回时,我们需要将PyObject返回到 Python 运行时。具体来说,从清单 7-1 中我们可以看到,包装函数总是接受两个PyObject参数,习惯上称为selfargsself参数指向模块级函数的模块对象(这里就是这种情况);对于类方法,它指向对象实例(即调用调用的对象)。我们在这个项目中不使用这个论点,所以我们忽略它。对于args参数,我们区分两种情况。对于只有一个参数的函数,这将直接在 Python 对象中传递。在函数有多个参数的情况下,args参数指向一个tuple对象。tuple的每一项对应调用的参数列表中的一个参数。我们将在本章后面讨论如何解释tuple

描述统计学

看了函数声明之后,我们现在来看函数定义。我们从想要公开的最简单的函数开始。在底层 C++ 库中,函数GetDescriptiveStatistics(在 \StatsLib\Stats.h 中)被声明为接受两个参数,其中第二个是可选的。我们想从 Python 中调用这个函数,如清单 7-2 所示。

-

Listing 7-2Calling the DescriptiveStatistics function from Python

img/519150_1_En_7_Figb_HTML.png

从清单 7-2 ,即交互式 Python 会话,我们可以看到,我们首先用一个参数(data)调用函数,然后用两个参数(datakeys)。清单 7-3 显示了相应的 C++ 包装函数定义。

-

Listing 7-3The definition of the DescriptiveStatistics function

img/519150_1_En_7_Figc_HTML.png

该函数的结构很简单。第一部分涉及从args元组中提取PyObject指针。第二部分包括为底层 C++ 层翻译这些并返回结果。Python API 中的函数PyArg_ParseTuple检查参数类型,并将它们转换成 C/C++ 值。它使用一个模板字符串来确定所需的参数类型以及存储转换值的 C/C++ 变量的类型。模板字符串决定了元组如何解包它的参数。在这种情况下,我们告诉它有两个PyObject指针,这是由O(大写字母)表示的。分隔"O|O"字符串的“|表示第二个参数是可选的。稍后,我们将看到更多使用模板字符串的例子。下面的列表总结了模板字符串中使用的一些更常见的参数类型。

|

字符串

|

转换

| | --- | --- | | “我” | 将 Python 整数转换成int。 | | " l " | 将 Python 整数转换成long。 | | " d " | 将一个 Python 浮点数转换成一个double。 | | “哦” | 在一个PyObject指针中存储一个 Python 对象。 |

模板字符串中参数类型及其用法的完整列表如下: https://docs.python.org/3/c-api/arg.html

在这种情况下,正如我们所说的,两个参数都是 Python 对象。为了对它们做任何事情,我们的函数必须将它们转换成 C/C++ 类型。这里,使用函数ObjectToVector将第一个参数转换为std::vector<double>。类似地,我们使用ObjectToStringVector将第二个参数从PyObject转换为std::vector<std::string>。转换后的对象(_data_keys)被传递给本机 C++ 函数,结果被打包为std::unordered_map中的键值对返回。这些然后被转换回一个指向PyObject的指针,并返回给调用者。

线性回归

在更详细地查看转换函数之前,我们先来看看更多我们想要公开的函数。清单 7-4 显示了LinearRegression的包装函数。

-

Listing 7-4The wrapper function for LinearRegression

img/519150_1_En_7_Figd_HTML.png

我们可以在清单 7-4 中看到,这个函数遵循与我们之前看到的DescriptiveStatistics函数相似的结构。然而,在这种情况下,args参数包含两个非可选项目。因此,模板字符串是"OO"。这些代表执行操作所需的两个数据集。在调用原生 C++ 函数之前,我们需要将args元组解包成有效的PyObjectxsys。然后我们需要将每一项转换成适当的 C++ 类型。一旦完成,我们调用底层的 C++ 函数并返回结果。

统计测试

对于统计测试函数,我们以与前面相同的方式构造函数。然而,在这种情况下,包装函数在堆栈上创建了一个TTest类的实例。为此,它需要将参数传递给相应的构造函数。清单 7-5 中显示了一个这样的例子。

-

Listing 7-5The SummaryDataTTest wrapper function

img/519150_1_En_7_Fige_HTML.png

到目前为止,我们已经看到了PyObject* args元组被传入PyArg_ParseTuple,参数被提取为PyObjects。然而,在清单 7-5 中,我们利用了标准转换。来自样本数据构造器的 t-test 需要四个double。因此,我们使用带有模板字符串"dddd"PyArg_ParseTuple来解包args元组,以表示四个双精度值。因为这些是内置类型,函数PyArg_ParseTuple隐式地转换它们。无需进一步转换。然后,该函数继续创建TTest实例,并调用Perform进行计算,然后调用Results获得结果包。然后将它转换回 Python 字典。

OneSampleTTestTwoSampleTTest的处理方式相似。OneSampleTTest如清单 7-6 所示。

-

Listing 7-6The OneSampleTTest wrapper function

img/519150_1_En_7_Figf_HTML.png

从清单 7-6 和之前的清单中,我们可以看到函数OneSampleTTestTwoSampleTTest的相似结构。我们首先声明我们期望从args中得到的类型。然后我们使用带有适当模板字符串的PyArg_ParseTuple将参数解包成内置类型或PyObject指针。然后,在将结果返回给 Python 之前,我们进行所需的任何进一步的转换。在OneSampleTTest的情况下,模板字符串是"dO",表示第一个参数是 double,第二个参数是PyObject。因此,我们对第一个参数(double mu0)使用标准转换,并将第二个参数解包为一个PyObject指针,然后它被转换为一个std::vector<double>,如我们之前所见。

转换层

我们已经看到,对于内置类型(boolintdouble,等)。),我们不需要做什么特别的事情。转换由PyArg_ParseTuple使用适当的模板字符串参数来处理。对于 STL 类型,转换层(conversion . h/conversion . CPP)为类型转换逻辑提供了一个中心位置。只有三个功能。一个用于将代表 Python 的PyObject转换为std::vector<double>。第二个函数是将代表 Python 字符串的PyObject转换成std::vector<std::string>。最后,我们有一个函数将结果(字符串键和数值的无序映射)转换成一个PyObject指针。清单 7-7 显示了ObjectToVector功能。

-

Listing 7-7Converting a PyObject to a std::vector<double>

img/519150_1_En_7_Figg_HTML.png

查看清单 7-7 中的代码,我们看到从 Python 对象到 STL 类型的转换非常简单。我们期望来自 Python 的参数是一个list,所以我们需要使用PyList_xxx函数。首先,我们获取输入列表的大小,然后使用 for 循环提取每一项。我们使用函数PyList_GetItem来检索索引数据项,并根据需要将其从 Python 编号转换为double ( PyFloat_AsDouble)。然后将其放入std::vector<double>中。当循环完成时,数据被返回给调用者。

清单 7-8 显示了三个函数中的第二个,ObjectToStringVector

-

Listing 7-8The ObjectToStringVector function

img/519150_1_En_7_Figh_HTML.png

清单 7-8 中显示的ObjectToStringVector函数将 Python 列表转换为字符串向量。我们可以看到这和前面的函数类似。在这种情况下,我们首先检查输入对象是否有效。我们知道这里的PyObject代表一个可选参数,所以参数有可能是 null。在前一种情况下,如果不提供参数,函数PyArg_ParseTuple就会失败,所以检查是多余的。然而,我们应该意识到,如果我们扩展了ObjectToVector函数的用法(特别是允许可选数据),那么我们需要改变这一点。检查之后,我们继续从列表中提取有效的PyObject。这种情况下的不同之处在于,我们需要将其转换为字符串。为了简单起见,我们不检查 Python 字符串是 Unicode UTF-8 还是 UTF-16。我们简单地假设 UTF-8 并使用 Python 函数PyUnicode_1BYTE_DATA将字符串转换成char*,然后在std::string的构造函数中使用它。执行转换的一种更健壮的方法是检查各种可能的 Python 类型,并相应地处理这些情况。以一种通用的方式处理字符串,并在一个跨平台的环境中工作,这是一个非常大的话题,超出了本章的范围。

清单 7-9 展示了最后一个函数MapToObject,它将结果地图转换为 Python 字典。

-

Listing 7-9Converting the underlying results package to a Python dictionary

img/519150_1_En_7_Figi_HTML.png

正如我们在清单 7-9 中看到的,MapToObject函数比以前稍微复杂一些。在这种情况下,我们将对结果图的引用作为输入。代码做的第一件事是创建一个新的 Python 字典。然后,我们迭代结果项,并将每个项插入字典中。密钥是字符串,所以我们使用PyUnicode_FromString来执行转换。为了获得该值,我们使用PyFloat_FromDouble(就像我们之前做的那样)。最后,我们将提取的键值对设置为一个字典项,并检查这是否成功。在这种情况下,我们需要减少键和值的引用计数,因为我们不再使用PyObject引用。我们使用Py_XDECREF宏,它允许对象为空,而不是Py_DECREF。如果PyDict_SetItem没有成功,我们还需要减少字典的引用计数,并返回nullptr以向 Python 运行时指示失败。

这个上下文中的引用计数非常复杂,但是为了避免 Python 的内存泄漏,引用计数是必需的。但是,对引用计数的全面讨论超出了本章的范围。Python 文档包含关于该主题的有用部分。

现在很清楚,转换层可以做得更通用,从而得到改进。我们的三个转换函数完全特定于底层原生 C++ 层的需求。如果能对一些代码进行一般化就好了。特别是,使用 RAII(资源获取是初始化)来管理引用计数似乎是一种有用的方法。此外,处理默认参数以及在标准库容器和模板之间进行转换也是有益的。虽然这可能很诱人,但在 C++ 和 Python 之间编写一个“通用”转换层可能很难做到,而且实现起来很耗时。附加资源部分提供了许多关于这些主题的参考资料。幸运的是,正如我们将在下一章看到的,两者都有所促进。Python 和 PyBind 在这方面做得非常出色。

错误处理

我们知道 StatsLib 抛出异常。如果我们不处理这些,有可能从 Python 脚本中,我们只是终止了 Python 外壳。这不一定是我们想要的。因此,和以前一样,我们使用代码将STATS_TRY/STATS_CATCH宏封装到函数调用中,将std::exception转换成 Python 可以解释的信息字符串。

STATS_TRY/STATS_CATCH宏的定义如清单 7-10 所示。

-

Listing 7-10Handling exceptions

img/519150_1_En_7_Figj_HTML.jpg

在清单 7-10 中,在一个异常被抛出后,我们首先使用PyErr_SetString函数指出一个错误情况。根据文档,传递给这个函数的对象不需要Py_DECREF(https://docs.python.org/3/extending/extending.html#refcounts)。第一个参数是 Python 异常对象,第二个是来自 C++ 函数的信息字符串。第二阶段是通过返回nullptr来指示失败。如果我们想更进一步,我们可以为这个模块创建一个标准的异常,或者我们可以用一个自定义类来扩展异常处理。然而,对异常处理主题的全面讨论超出了本章的范围。附加资源部分提供了更多的参考资料。

既然我们在 C++ 层处理异常,我们可以在 Python 脚本中添加等效的异常处理。例如,我们将DescriptiveStatistics函数包装在try/except块中,并报告任何异常。报告异常后,代码继续正常执行。清单 7-11 显示了代码。

-

Listing 7-11Reporting exceptions from Python

img/519150_1_En_7_Figk_HTML.jpg

清单 7-11 中的代码很简单。我们接受包装函数抛出的异常,并输出信息字符串。一个典型的交互式会话展示了这种情况是如何出现的:

>>> import StatsPythonRaw as Stats
>>> data = []
>>> print(Stats.DescriptiveStatistics(data))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: Insufficient data to perform the operation.

正如我们所看到的,我们捕获并报告 C++ 异常,并将其返回给 Python,在 Python 中我们可以继续交互会话。

模块定义

到目前为止,我们已经讨论了调用函数和类型转换。Python 扩展模块组件需要的最后一部分是模块定义。代码位于 module.cpp 中。代码由三个主要部分组成:导出函数、模块定义和初始化函数。我们将依次了解每一项。清单 7-12 显示了导出的函数。

-

Listing 7-12Exporting functions from the module

img/519150_1_En_7_Figl_HTML.png

导出的函数(列表 7-12 )在一个struct中定义,它包含一个我们想要导出的函数数组。参数很简单。第一个参数是向 Python 公开的函数的名称。第二个参数是实现它的函数。该功能必须符合typedef:

typedef PyObject *(*PyCFunction)(PyObject *, PyObject *)

这声明了一个函数,它将两个PyObject指针作为参数,并返回一个PyObject指针。这对应于我们如何声明我们的函数(参见清单 7-1 )。第三个参数是描述函数所期望的argsMETH_xxx标志的组合。对于单参数函数,我们将使用METH_O。这意味着该函数只接受一个PyObject参数。采用METH_O的函数直接在args参数中传递PyObject。所以,没必要用PyArg_ParseTupleMETH_VARARGS表示函数接受可变数量的参数,这些参数需要由PyArg_ParseTuple解包。最后一个参数是 Python docstring 属性(__doc__),也可以是一个nullptr

导出的函数列表后面是模块定义结构,如清单 7-13 所示。

-

Listing 7-13The module definition structure

img/519150_1_En_7_Figm_HTML.png

清单 7-13 所示的PyModuleDef结构定义了模块的结构。我们用符号PyModuleDef_HEAD_INIT初始化这个结构。接下来,我们提供用于 Python 导入语句的模块名,后面是模块描述。我们使用的最后一个参数是指向定义导出方法的结构的指针(清单 7-12 中定义的结构)。按照模块定义,我们用模块的名字定义函数PyInit_StatsPythonRaw。这与模块定义中声明的名称相匹配是很重要的(即*“statspithornaw”*)。Python 运行时环境将寻找这个函数来调用,以便在这个模块被import调用时执行这个模块的初始化。这就是这个组件。我们现在可以建造它。它应该不会出现警告或错误。输出文件(statsphythonraw . pyd)将被复制到 StatsPython 项目中,我们可以在 Python 脚本中导入它。

Python 客户端

既然我们已经有了一个封装了本地 C++ 函数的 Python 扩展模块,我们就可以用我们选择的任何方式来使用它。我们可以推出一款 Jupyter 笔记本,并在另一个项目中使用我们的功能。或者作为对我们已经公开的函数的快速测试,我们也可以运行 Python 的 shell 并交互式地使用读取-评估-打印循环(REPL)。清单 7-14 显示了一个小例子。

-

Listing 7-14Performing a summary data t-test

img/519150_1_En_7_Fign_HTML.png

正如我们从清单 7-14 中看到的,这提供了一种简单的方法来实现这些功能。作为一个稍微复杂一点的替代方案,我们可以使用脚本statspithornaw . py。这提供了更广泛的功能测试。该脚本定义了一个主函数,因此我们可以直接从命令提示符(> python StatsPythonRaw.py)运行它。例如,我们也可以直接在 Visual Studio 代码中打开它,并执行它(F5)。主要功能如清单 7-15 所示。

-

Listing 7-15The main function exercising StatsPythonRaw

img/519150_1_En_7_Figo_HTML.jpg

清单 7-15 中的脚本定义了我们在别处使用过的xsys数据集。我们将这些作为描述性统计和线性回归函数的输入。接下来,我们调用ttest_summary_data函数。代码的最后一部分使用 Pandas 将数据加载到数据框中。使用这些数据,我们进行了单样本 t 检验。最后,我们加载 us-mpgjp-mpg 数据集。这些数据集与我们在 StatsViewer MFC 应用程序中使用的数据集相同。我们使用 matplotlib 可视化箱线图中均值的差异(图 7-1 ),之后我们执行双样本 t 检验。

img/519150_1_En_7_Fig1_HTML.png

图 7-1

对比美国和日本汽车汽油消耗的箱线图

排除故障

调试 Python 和 C++ 出人意料地简单。VSCode IDE 可以很好地处理 Python 调试。调试 C++ 代码只需在 VSCode 中的 Python 脚本中放置一个断点(Python 执行将在那里暂停),然后使用 Visual Studio(调试➤附加到进程...)附加到正确的 Python 托管进程(您可以使用procexp64.exe来轻松识别这一点)。然后,脚本将跳转到 C++ 代码中,在适当的位置中断(假设您正在调用 C++ 模块的调试版本)。从这里开始,您可以逐句通过 C++ 代码。这只是许多可能性中的一种。附加资源部分提供了更多的细节。

摘要

在本章中,我们从头开始构建了一个基本的 Python 模块。我们首先公开底层 StatsLib 中的函数,然后根据需要转换类型。最后,我们定义了模块。这可以被认为是我们公开的函数和参数的声明性描述(元数据)。在使用 Python 模块方面,我们创建了一个简单的脚本。除了练习一些功能,我们还演示了模块如何与 Pandas 和 matplotlib 互操作。

正如我们在本章的介绍中指出的,使用“原始”CPython 编写一个模块是有益的,因为它说明了使用低级方法将 C/C++ 连接到 Python 所涉及的一些困难。我们不得不编写特定的代码来处理我们需要的转换。此外,我们已经看到,每当我们与PyObject s 交互时,我们都需要注意引用计数。Python 和 PyBind 框架,看看这两者如何减轻我们在这里看到的困难。

额外资源

以下资源有助于了解本章所涵盖主题的更多信息:

练习

本节中的练习通过我们的 CPython 扩展模块向 Python 展示新的 C++ 功能。

1)为 z 测试函数添加过程包装。这些函数应该与 t-test 函数几乎相同。不需要额外的转换函数,因此您可以用一种简单的方式来修改 t-test 函数。

  • 在 *Functions.h,*中添加三个函数的声明:

    // Wrapper function for a z-test with summary input data (no sample)
    PyObject* SummaryDataZTest(PyObject* /* unused module reference */, PyObject* args);
    
    // Wrapper function for a one-sample z-test with known population mean
    PyObject* OneSampleZTest(PyObject* /* unused module reference */, PyObject* args);
    
    // Wrapper function for a two-sample z-test
    PyObject* TwoSampleZTest(PyObject* /* unused module reference */, PyObject* args);
    
    
  • Functions.cpp 中,添加这些函数的实现。遵循 t-test 包装函数的代码。

  • module.cpp 中,向数组添加三个新函数:

    static PyMethodDef StatsPythonRaw_methods[] =
    {
    //...
    }
    
    
  • 构建 StatsPythonRaw。它的构建应该没有警告和错误。这些函数现在应该可以从 Python 中调用了。以交互方式尝试这些功能,例如:

    >>> import StatsPythonRaw as Stats
    >>> results: dict = Stats.SummaryDataZTest(5, 6.7, 7.1, 29)
    >>> print(results)
    {'z': 1.2894056580462898, 'sx1': 7.1, 'pval': 0.19725709541241007, 'x1-bar': 6.7, 'n1': 29.0}
    
    
  • 在 VSCode 中打开 StatsPython 项目。打开statspithonraw . py脚本。添加函数以使用我们之前使用的数据测试 z 测试函数。

2)添加一个过程包装函数来计算一个简单的MovingAverage。我们从之前的练习中知道,在处理时间序列移动平均函数时,我们需要添加一些转换函数。具体来说,在这个 Python 例子中,我们需要将一个PyObject转换成一个long的向量。此外,我们需要将来自std::vector<double>的结果转换成一个 Python list

添加转换函数所需的步骤如下:

  • 在 *Conversion.h,*中添加一个转换函数的声明:

    std::vector<long> ObjectToLongVector(PyObject* o);
    
    
  • 在 *Conversion.cpp 中,*添加实现。这类似于ObjectToVector,但是它使用PyLong_AsLongPyObject中提取长值。

类似地,我们需要将结果(一个矢量double s)转换成一个 Python list

  • Conversion.h 中,添加声明:

    PyObject* VectorToObject(const std::vector<double>& results);
    
    
  • In Conversion.cpp, add the following implementation:

    PyObject* VectorToObject(const std::vector<double>& results)
    {
        const std::size_t size = results.size();
        PyObject* list = PyList_New(size);
    
        for(std::size_t i = 0; i < size; ++i)
        {
            double d = results[i];
            int success = PyList_SetItem(list, i, Py_BuildValue("d", d));
            if (success < 0)
            {
                Py_XDECREF(list);
                return nullptr;
            }
        }
        return list;
    }
    
    

    在这种情况下,我们使用输入向量大小创建一个新的 Python list。为了设置列表项,我们使用了函数PyList_SetItem。我们使用模板字符串"d"(用于double)将便利函数Py_BuildValue返回的PyObject传递给它。

转换函数就绪后,编写包装函数。编写包装函数所需的步骤如下:

  • 在 *Functions.h,*中增加一个声明:

    PyObject* MovingAverage(PyObject* /* unused module reference */, PyObject* args);
    
    
  • Functions.cpp 中,有许多细节:

    • #include "TimeSeries.h"添加到文件的顶部。

    • 添加实现。该函数采用三个非可选参数。日期列表、观察列表和窗口大小。

    • 添加异常处理程序STATS_TRY/STATS_CATCH

    • 声明输入参数:

      PyObject* dates = nullptr;
      PyObject* observations = nullptr;
      long window{ 0 };
      
      
    • 用模板字符串"OOl"解析输入args

    • 像以前一样转换输入以构建时间序列。

    • 使用VectorToObject转换功能返回结果。

  • Finally, in module.cpp, add the new function to the list of exposed functions:

    {
        "MovingAverage",
        (PyCFunction)API::MovingAverage,
        METH_VARARGS,
        "Compute a simple moving average of size = window."
    },
    
    

    这就完成了为MovingAverage函数添加过程包装器的代码。

  • 构建 StatsPythonRaw。它的构建应该没有警告和错误。

  • 以交互方式尝试该功能,例如:

    >>> import StatsPythonRaw as Stats
    >>> dates: list = list(range(1, 16))
    >>> observations: list = [1, 3, 5, 7, 8, 18, 4, 1, 4, 3, 5, 7, 5, 6, 7]
    >>> sma: list = Stats.MovingAverage(dates, observations, 3)
    >>> print(sma)
    
    
  • 在 VSCode 中打开 StatsPython 项目。打开statspithonraw . py 脚本。添加一个函数来测试移动平均线,包括异常处理。运行脚本,并根据需要进行调试。

Footnotes 1

来源: https://docs.microsoft.com/en-us/visualstudio/python/working-with-c-cpp-python-in-visual-studio?view=vs-2019

 

八、使用 Boost.Python 和 PyBind 开发模块

介绍

在前一章中,我们看到了如何创建一个基本的 Python 扩展模块。我们添加了代码来公开底层 C++ 统计函数库的功能。我们看到了如何在PyObject指针和本地 C++ 类型之间进行转换。虽然并不特别困难,但我们发现它很容易出错。在本章中,我们考虑两个框架——Boost。Python 和 py bind——它们克服了这些困难,使得 Python 扩展模块的开发更加容易。我们构建了两个非常相似的包装组件,第一个基于 Boost。Python 和 PyBind 上的第二个。这里的目的是比较这两个框架。接下来,我们看一个典型的 Python 客户机,并开发一个脚本来测量扩展模块的相对性能。我们用一个简单的 Flask 应用程序来结束这一章,它演示了如何使用我们的 PyBind 模块作为(有限的)统计服务的一部分。

助推。计算机编程语言

Boost Python 库是一个连接 Python 和 C++ 的框架。它允许我们使用框架提供的类型,以非侵入的方式向 Python 公开 C++ 类、函数和对象。我们可以继续使用提供的类型在包装层中编写“常规”C++ 代码。Boost Python 库非常丰富。它支持 Python 类型到 Boost 类型的自动转换、函数重载和异常翻译等。使用 Boost。Python 允许我们在 C++ 中轻松操作 Python 对象,与我们在前一章看到的低级方法相比,简化了语法。

先决条件

除了安装 Boost(我们在这个项目中使用 Boost 1.76),我们还需要一个构建版本的库。具体来说,我们需要 Boost Python 库。助推。与大多数 Boost 库功能不同,Python 不是一个只有头文件的库,所以我们需要构建它。此外,我们需要确保当我们构建库时,Boost 的版本。Python 库与我们的目标 Python 版本一致。我们一直在使用 Python 3.8,所以我们希望下面的 Boost 库能够出现:

  • \ boost _ 1 _ 76 _ 0 \ stage \ lib \ lib boost _ python 38-VC 142-mt-GD-x32-1 _ 76 . lib

  • \ boost _ 1 _ 76 _ 0 \ stage \ lib \ lib boost _ python 38-VC 142-mt-x32-1 _ 76 . lib

  • \ boost _ 1 _ 76 _ 0 \ stage \ lib \ lib boost _ python 38-VC 142-mt-GD-x64-1 _ 76 . lib

  • \ boost _ 1 _ 76 _ 0 \ stage \ lib \ lib boost _ python 38-VC 142-mt-x64-1 _ 76 . lib

这些库的 Boost 安装和构建过程在附录 A 中有更详细的描述。

项目设置

StatsPythonBoost 项目是一个标准的 Windows DLL 项目。和以前一样,该项目引用 StatsLib 静态库。表 8-1 总结了项目设置。

表 8-1

StatsPythonBoost 的项目设置

|

标签

|

财产

|

价值

| | --- | --- | --- | | 一般 | C++ 语言标准 | ISO C++17 标准版(/std:c++17) | | C/C++ >常规 | 其他包含目录 | <用户\用户>\蟒蛇 3 \包含*(BOOSTROOT)(* *BOOST_ROOT)**(解决方案目录)通用\包含* | | 链接器>常规 | 附加库目录 | <用户\用户> \Anaconda3\libs*$(BOOST_ROOT)\stage\lib* | | 生成事件>后期生成事件 | 命令行 | (参见下文) |

从表 8-1 中可以看出,该项目设置与之前的项目类似。在这种情况下,我们没有重命名目标输出。我们把这个留给后期构建脚本(参见下文)。在附加的包含目录中,我们引用了 Python.h 和 StatsLib 项目包含目录的位置。另外,我们用$(BOOST_ROOT)宏引用 Boost 库。类似地,在附加的库目录中,我们添加了对 Python 库和 Boost 库的引用。

和前面的项目一样,我们走捷径。我们没有在 Python 环境中安装这个库,而是简单地将输出复制到我们的 Python 项目位置( \StatsPython )。从那里,我们可以用 Python 脚本或交互方式导入库。在后期构建事件中,我们将 dll 复制到脚本目录,删除之前的版本,并将 dll 重命名为*。pyd* 扩展,如下:

copy /Y "$(OutDir)$(TargetName)$(TargetExt)" "$(SolutionDir)StatsPython\$(TargetName)$(TargetExt)"
del "$(SolutionDir)StatsPython\$(TargetName).pyd"
ren "$(SolutionDir)StatsPython\$(TargetName)$(TargetExt)" "$(TargetName).pyd"

有了这些设置,一切都应该没有警告或错误。

代码组织

Visual Studio Community Edition 2019 为 Windows dll 生成的项目会生成一些我们忽略的文件。我们忽略了 dllmain.cpp 文件(它包含标准 Windows dll 的入口点)。我们还忽略了文件 framework.hpch.cpp (除了它包含了 pch.h ,即预编译头文件)。

pch.h 文件中,我们有

#define BOOST_PYTHON_STATIC_LIB
#include <boost/python.hpp>

宏指示在这个 dll 模块中,我们静态链接到 Boost Python:

\ boost _ 1 _ 76 _ 0 \ stage \ lib \ lib boost _ python 38-VC 142-mt-...-...-1_76.lib

“…”取决于特定的处理器架构,尽管在我们的例子中我们只针对 x64。第二行引入了所有的 Boost Python 头。代码的其余部分像以前一样被组织成三个主要区域:函数(functions . h/functions . CPP)、转换层(conversion . h/conversion . CPP)和模块定义。此外,对于这个项目,我们有一个包装类statisticaltests . h/statisticaltests . CPP来包装 t-test 功能。我们将依次讨论这些领域。

功能

API名称空间中,我们声明了两个函数:DescriptiveStatisticsLinearRegression。两个函数都接受相应的boost::python参数。助推。Python 附带了一组对应于 Python 的派生对象类型:

巨蟒型助推型

  • list        boost::python::list

  • dict        boost::python::dict

  • tuple    boost::python::tuple

  • str        boost::python::str

正如我们将要看到的,这使得转换成 STL 类型变得非常简单。函数内部的代码也很简单。我们首先将参数转换成 StatsLib 可用的类型。然后我们调用底层的 C++ 函数,收集结果,并把它们翻译成 Python 可以理解的形式。提升。Python 库使这变得非常简单和灵活。清单 8-1 展示了DescriptiveStatistics函数的实现。

-

Listing 8-1The DescriptiveStatistics wrapper function

img/519150_1_En_8_Figa_HTML.png

清单 8-1 中的DescriptiveStatistics函数看起来应该很熟悉。它遵循与前一章中的原始 Python 示例相同的结构。函数声明中的主要区别是,我们可以使用 Boost 中定义的类型来代替PyObject指针。Python 库。在这种情况下,两个参数都作为对一个boost::python::listconst引用被传入。第二个参数是默认的,因为我们希望能够调用DescriptiveStatistics,不管有没有键。输入参数分别被转换成一个std::vector<double>和一个std::vector<std::string>。然后在调用底层统计库函数时使用这些函数。结果包像以前一样被返回(一个std::unordered_map<std::string, double>类型)并被转换成一个boost::python::dict

清单 8-2 显示了LinearRegression函数的代码。

-

Listing 8-2The LinearRegression wrapper function

img/519150_1_En_8_Figb_HTML.png

从清单 8-2 中可以看出,LinearRegression函数遵循与前面相同的结构。该函数接收两个列表,将它们转换成相应的数据集,调用底层函数,并将结果包转换成 Python 字典。

统计测试

API名称空间中,我们为三个统计假设检验函数创建了一个单独的名称空间StatisticalTests。与“原始”情况一样,这里我们最初选择将TTest类的用法包装在一个函数中。清单 8-3 显示了汇总数据 t 检验函数。

-

Listing 8-3Wrapping up the TTest class in a function

img/519150_1_En_8_Figc_HTML.png

如清单 8-3 所示,为一个类提供过程化包装的方法很简单:我们获取输入数据并创建一个TTest类的实例(取决于函数调用和参数)。然后我们调用Perform进行计算,调用Results检索结果。这些然后被翻译回 Python 调用者。本例中的SummaryDataTTest函数接受与汇总数据 t-test 的构造函数参数相对应的四个参数。参数被打成对一个boost::python::objectconst引用。这为PyObject提供了一个包装器。然后,该函数利用boost::python::extract<T>(val)从参数中获取一个double值。一般来说,语法比使用PyArg_ParseTuple更干净、更直接。该函数的其余部分调用Perform并检索Results。和前面的DescriptiveStatisticsLinearRegression一样,它们被转换成一个boost::python::dict并返回给调用者。

转换层

正如我们前面看到的,对于内置类型(boolintdouble,等等),我们可以使用一个模板化的提取函数:

boost::python::extract<T>(val).

对于 STL 类型的转换,我们有三个inlined 函数。首先是一个模板函数to_std_vector。这就把代表 ?? 的 ?? 转换成了 ??。清单 8-4 显示了代码。

-

Listing 8-4Converting a boost::python::object list to a std::vector

img/519150_1_En_8_Figd_HTML.png

清单 8-4 从构造一个空的std::vector开始。然后,我们遍历输入列表,提取各个值,并将它们插入到向量中。我们使用这种基本方法来说明以标准方式访问列表元素。我们可以使用boost::python::stl_input_iterator<T>直接从迭代器中构造结果vector<T>。我们使用这个函数将一列double转换成一个double的向量,并将一列字符串键转换成一个string的向量

第二个功能是to_dict。这是一个专门的函数,用于将结果集转换为 Python 字典。清单 8-5 显示了代码。

-

Listing 8-5Converting the results package to a Python dictionary

img/519150_1_En_8_Fige_HTML.png

在这种情况下,我们输入一个对std::unordered_map<std::string, double>const引用,并通过简单地迭代结果将内容返回到一个boost::python::dict中。最后一个功能是to_list。这类似于前面的to_dict功能。在这种情况下,我们创建一个 Python list,并从矢量double中填充它。

模块定义

我们的助力。Python 模块在 module.cpp 中定义。模块定义包括我们希望向 Python 公开的函数和类。我们将依次处理每一个问题。清单很长,所以被分成两部分。首先,清单 8-6a 显示了公开这些函数的代码。

-

Listing 8-6aThe functions: StatsPythonBoost module definition

img/519150_1_En_8_Figf_HTML.png

在清单 8-6a 中,模块定义的这一部分应该看起来有些熟悉。它与我们在前一章中看到的“原始”方法没有太大的不同。我们使用boost::python::def函数来声明我们正在包装的函数。第一个参数是我们想从 Python 调用的函数名。第二个参数是函数地址。最后一个参数是docstring。正如前面针对DescriptiveStatistics函数所指出的,我们希望能够在有键和无键的情况下从 Python 中调用它,并让它像下面的交互会话演示的那样运行:

>>> import StatsPythonBoost as Stats
>>> data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> results = Stats.DescriptiveStatistics(data)
>>> print(results)
{'Mean': 4.5, 'Count': 10.0, 'Kurtosis': -1.2000000000000002, 'Skew.P': 0.0, ... }
>>> keys = ['Mean', 'StdDev.P']
>>> results = Stats.DescriptiveStatistics(data, keys)
>>> print(results)
{'Mean': 4.5, 'StdDev.P': 2.8722813232690143}

为了做到这一点,我们需要两个单独的重载函数。这和我们在第三章中使用的 C++/CLI 封装方法是一样的。然而,在这种情况下,我们不需要显式编写重载。我们利用宏BOOST_PYTHON_FUNCTION_OVERLOADS为我们生成重载。参数是生成器名称、我们想要重载的函数、最小参数数(本例中为 1)和最大参数数(本例中为 2)。定义好这个之后,我们将把f_overloads结构和docstring一起传递给def函数。

清单 8-6b 中显示的模块定义的第二部分声明了可以在 Python 中直接使用的类。

-

Listing 8-6bThe classes: StatsPythonBoost module definition

img/519150_1_En_8_Figg_HTML.png

清单 8-6b 显示了我们在这个模块中包装的TTestDataManager类。定义了这些类后,我们可以从 Python 脚本中编写以下内容,例如:

# Perform t-test from summary data
t: Stats.TTest = Stats.TTest(5, 9.261460, 0.2278881e-01, 195)
t.Perform()
print(t.Results())

t-test 的 C++ 包装类在 StatisticalTests.h 中定义。类模板参数引用了我们的包装类。在这种情况下,我们将其命名为StudentTTest,以区别于底层的Stats::TTest类。这个类拥有底层Stats::TTest类的一个实例。构造函数确定要执行的 t-test 的类型,并使用我们已经看到的相同转换在boost::python类型和底层 C++ 类型之间进行转换。

从清单 8-6b 中的模块定义中,我们可以看到第一个参数是类名"TTest"。这是我们将从 Python 调用的类型的名称。此外,我们定义了一个带有四个参数的init函数(构造函数)。然后,我们定义两个额外的init函数,每个函数用于剩余的带有相应参数的构造函数。最后,我们定义两个函数PerformResults。所有的功能都提供了一个docstring。这就是我们向 Python 公开原生 C++ 类型所需做的全部工作。

DataManager类以类似的方式公开。C++ 包装类在名称空间API::Data中的 DataManager.h 中定义。这允许我们将包装类与同名的 StatsLib C++ 类分开。和以前一样,包装类的目的是处理类型转换和管理 StatsLib 中底层DataManager类的生命周期。清单 8-7 显示了一个典型的示例函数。

-

Listing 8-7The DataManager::ListDataSets function

img/519150_1_En_8_Figh_HTML.png

从清单 8-7 中,我们可以看到函数ListDataSets使用 Boost 返回一个 Python list。Python 类型。该列表包含被键入为的Stats::DataSetInfo

using DataSetInfo = std::pair<std::string, std::size_t>;

这些项目包含数据集名称和数据中的观测值数量。该函数首先从该类包装的成员m_manager中获取当前加载的数据集。在 for 循环中,我们使用函数boost::python::make_tuple创建一个包含数据集信息的 Python tuple元素。然后将它追加到结果列表中,并返回给调用者。其余的功能同样简单明了。

异常处理

和前一章一样,异常应该从包装函数中处理。特别是,我们关心错误的参数,所以我们应该检查类型并适当地报告异常。我们可以使用与上一章相同的方法(手动将 C++ 异常转换成 Python 异常)。但是,我们也可以利用 Boost.Python。Python 框架将我们的函数包装在对.def(...)的调用中,这样就不会通过 Python 直接调用它们。而是 Python 调用function_call(...)(\ boost _ 1 _ 76 _ 0 \ libs \ Python \ src \ object \ function . CPP)。这个函数将实际的函数调用包装在一个异常处理程序中。异常处理程序以我们之前的方式(\ boost _ 1 _ 76 _ 0 \ libs \ python \ src \ errors . CPP)处理异常,尽管它捕获并转换更多的异常类型。这意味着 Python 不会暂停,异常会得到很好的处理。我们可以使用下面的 Python 代码对此进行测试,该代码在list中传递一个字符串,而不是预期的数字项:

try:
    x = [1, 3, 5, 'f', 7]
    summary: dict = Stats.DescriptiveStatistics(x)
    print(summary)
except Exception as inst:
    report_exception(inst)

报告的错误是

<class 'TypeError'>
No registered converter was able to produce a C++ rvalue of type double from this Python object of type str

此错误由 Boost 提供。另一方面,如果我们传入一个空数据集,我们会得到以下结果:

try:
    x = []
    summary: dict = Stats.DescriptiveStatistics(x)
    print(summary)
except Exception as inst:
    report_exception(inst)

报告的错误是

<class 'ValueError'> The data is empty.

这是从基础 StatsLib 中引发的错误。基本上,我们在前一章写的错误处理现在是免费提供的。

皮巴弟

在本节中,我们将开发第三个也是最后一个 Python 扩展模块。这次我们使用 PyBind。助推。Python 已经存在很长时间了,它所属的 Boost 库提供了广泛的功能。如果我们想要做的只是创建 Python 扩展模块,这就使得它成为一个相对重量级的解决方案。PyBind 是一个轻量级的替代方案。它是一个只有头文件的库,提供了大量的函数来帮助编写 Python 的 C++ 扩展模块。PyBind 可从这里获得: https://github.com/pybind/pybind11

先决条件

本节的唯一先决条件是将 PyBind 安装到 Python 环境中。您可以在命令提示符下使用pip install pybind。或者可以下载转轮( https://pypi.org/project/pybind11/#files )运行pip install "pybind11-2.7.0-py2.py3-none-any.whl"

项目设置

StatsPythonPyBind 项目的设置方式与前一个项目类似。这是一个标准的 Windows DLL 项目。表 8-2 总结了项目设置。

表 8-2

StatsPythonPyBind 的项目设置

|

标签

|

财产

|

价值

| | --- | --- | --- | | 一般 | C++ 语言标准 | ISO C++17 标准版(/std:c++17) | | C/C++ >常规 | 其他包含目录 | <用户\用户>\蟒蛇 3 \包含*<用户>\ AppData \ Roaming \ Python \ Python 37 \ site-packages \ pybind 11 \ include**(解决方案目录)通用\包链接器>常规附加库目录<用户\用>\Anaconda3\libs(解决方案目录)通用\包含* | | 链接器>常规 | 附加库目录 | *<用户\用户> \Anaconda3\libs**(BOOST_ROOT)\stage\lib* | | 生成事件>后期生成事件 | 命令行 | (参见下文) |

我们像以前一样创建一个模块,复制到脚本目录并重命名为*。pyd* 。我们使用以下脚本:

del "$(SolutionDir)StatsPython\$(TargetName).pyd"
copy /Y "$(OutDir)$(TargetName)$(TargetExt)" "$(SolutionDir)StatsPython\$(TargetName)$(TargetExt)"

此外,我们已经删除了 pch 文件,并将项目设置为不使用预编译头文件。最后,我们在项目引用中添加了对 StatsLib 项目的引用。在这一点上,所有的构建都应该没有警告或错误。

代码组织:module.cpp

在这个项目中,只有一个文件, module.cpp 。这个文件包含所有的代码。正如我们在上一节 Boost 中看到的那样。Python 和上一章一样,我们通常将转换层从包装的函数和类中分离出来。我们已经将这些从模块定义中分离出来。这是一种在包装层组织代码的便捷方式,并允许我们适当地分离关注点(如转换类型或调用函数)。然而,PyBind 简化了这两个方面。

在文件 module.cpp 的顶部,我们包含了 PyBind 头:

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>

接下来是我们的 StatsLib includes。

以前,我们必须声明接受 Python 类型作为参数(或者是PyObject或者是boost::python::object)的包装器/代理函数,并将它们转换成底层的原生 C++ 类型。使用 PyBind,我们不需要这样做。我们现在有了一个定义模块的宏PYBIND11_MODULE。清单很长,所以我们把它分成三个部分。第一部分处理我们公开的函数,接下来的两部分处理我们公开的类。我们公开的函数如清单 8-8a 所示。

-

Listing 8-8aThe function definitions in the StatsPythonPyBind module

img/519150_1_En_8_Figi_HTML.png

PYBIND11_MODULE宏定义了 Python 在导入语句中使用的模块名statspithonpybind。在模块定义中,我们可以看到DescriptiveStatisticsLinearRegression函数的声明。.def(...)函数用于定义导出的函数。就像之前一样,我们给它一个从 Python 调用的名字,最后一个参数是一个docstring

然而,与以前不同,我们不需要单独的包装函数。我们可以简单地提供底层函数地址。这是第二个参数。参数和返回类型的转换由 PyBind 框架处理。对于有第二个默认参数的Stats::GetDescriptiveStatistics函数,我们可以提供关于参数结构的更多信息。具体来说,PyBind 允许我们指定参数和缺省值(如果需要的话),所以我们在函数地址后添加参数,py::arg("data")py::arg("keys")缺省为所需的值。接下来,三个功能SummaryDataTTestOneSampleTTestTwoSampleTTest现在完全没有必要了。我们提供的包装器仅用于说明。双样本 t-test 包装器的代码如下:

std::unordered_map<std::string, double> TwoSampleTTest(const std::vector<double>& x1, const std::vector<double>& x2)
{
    Stats::TTest test(x1, x2);
    test.Perform();

    return test.Results();
}

这里重要的不是函数如何包装TTest类,而是包装函数使用原生 C++ 和 STL 类型作为函数参数和返回值。使用 Boost。Python,我们将不得不从/转换到boost::python::object。但是这里我们不再需要从 Python 类型转换到 C++ 类型。当然,如果我们愿意,我们可以显式包装函数。这是一个设计选择。

模块定义的第二部分处理类定义。清单 8-8b 中显示了TTest类。

-

Listing 8-8bThe description of the TTest class exported to Python

img/519150_1_En_8_Figj_HTML.png

清单 8-8b 展示了底层 C++ StatsLib 中的TTest类是如何暴露给 Python 的。就像 Boost 的情况一样。Python,我们描述了我们想要使用的类型“TTest”。但是,在这种情况下,py::class_对象的模板参数是底层的Stats::TTest类。引用的类不是包装类,就像 Boost.Python 一样。在模板实参和参数传递给py::class_的构造函数后,我们使用.def函数来描述类的结构。在这种情况下,我们声明三个TTest构造函数,它们各自的参数作为模板参数传递给py::init<>函数。同样,值得强调的是,我们不需要做任何转换;我们只是传入原生 C++ 类型和 STL 类型(而不是boost::python::object类型)。最后,我们声明函数PerformResults,以及一个匿名函数,将对象的字符串表示返回给 Python。

DataManager类的定义同样简单。清单 8-8c 显示了类定义。

-

Listing 8-8cThe DataManager class definition

img/519150_1_En_8_Figk_HTML.png

从清单 8-8c 中我们可以看到,我们在.def函数中需要做的就是提供从 Python 使用的函数名到底层 C++ 函数的映射。除了在DataManager类中可用的函数之外,我们还可以访问构成 Python 类定义一部分的函数。例如,DataManager用一个定制的to_string函数扩展了__repr__函数,该函数输出关于数据集的内部信息。

正如我们在这个项目中看到的,包装器和“转换”层都是最小的。PyBind 提供了广泛的工具,允许我们轻松地将 C++ 代码连接到 Python。在这一章中,我们仅仅触及了表面。有大量的特性,我们只介绍了其中的一小部分。此外,我们意识到我们实际上只为最“普通”的情况编写了代码(利用了 PyBind 允许我们轻松做到这一点的事实)。

然而,虽然使用 PyBind 使 C++ 类和函数的公开变得简单明了,但我们需要意识到在幕后还有很多事情要做。特别是,我们需要知道可以传递给module_::def()class_::def()函数的返回值策略。这些注释允许我们为返回非平凡类型的函数调整内存管理。在这个项目中,我们只使用了默认策略return_value_policy::automatic。对这个主题的全面讨论超出了本章的范围。但是,正如文档所指出的,返回值策略是很复杂的,正确处理它们很重要。 1

如果我们后退一步,我们可以看到,在模块定义方面,两者都有所提升。Python 和 PyBind 为我们提供了定义 Python 实体的元语言。这似乎是一条复杂的路。可以说,用原生 Python 编写等价类比用元语言描述 C++ 类要容易一些。然而,我们在这里采用的描述原生 C++ 类的方法,显然解决了一个不同的问题,也就是说,它提供了一种(相对)简单的方法来导出 C++ 中的类,并在 Python 环境中以预期的方式管理它们。

除了定义函数和类,我们还小心地添加了文档字符串。这很有用,如果我们打印出这个类的帮助,就可以看到这些信息。这在清单 8-9 中显示了 StatsPythonPyBind 模块。

-

Listing 8-9Output from the Python help function for the TTest class

img/519150_1_En_8_Figl_HTML.png

清单 8-9 显示了使用内置help()函数的 StatsPythonPyBind 模块的输出。我们可以看到,它提供了对类方法和类初始化的描述,以及我们提供的docstring。它还提供了有关使用的参数类型和返回类型的详细信息。我们可以非常清楚地看到声明性 C++ 类描述是如何被翻译成 Python 实体的。StatsPythonBoost 的输出是相似的,尽管不完全相同,但值得比较。作为帮助功能的替代,我们可以使用inspect模块对我们的 Python 扩展进行自省。inspect模块提供了额外的有用功能来帮助获取关于对象的信息。如果您需要显示详细的回溯,这可能会很有用。正如所料,我们可以从模块中检索所有信息,当然,除了源代码。这两种方法都是为了说明,用有限的 C++ 代码,我们已经开发了一个合适的 Python 对象。

异常处理

正如所料,PyBind 框架提供了对异常处理的支持。C++ 异常,std::exception及其子类被翻译成相应的 Python 异常,并且可以在脚本中或由 Python 运行时处理。使用我们之前使用的两个示例中的第一个,来自 Python 的异常报告如下:

<class 'TypeError'>
DescriptiveStatistics(): incompatible function arguments. The following argument types are supported:
    1\. (arg0: List[float]) -> Dict[str, float] Invoked with: [1, 3, 5, 'f', 7]

异常处理提供了足够的信息来确定问题的原因,并且处理可以适当地进行。值得指出的是,PyBind 的异常处理能力超越了简单的 C++ 异常翻译。PyBind 提供了对几个特定 Python 异常的支持。它还支持注册自定义异常处理程序。PyBind 文档中介绍了详细信息。

Python“客户端”

既然我们已经构建了一个可以工作的 PyBind 模块,那么尝试一下其中的一些功能就很好了。我们当然可以创建一个全功能的 Python 应用程序。但是我们更喜欢保持简单和专注。和以前一样,我们不仅关注底层功能的实现,还关注与其他 Python 组件的互操作。与前几章不同,我们没有使用(几个)Python 测试框架中的一个来编写专门的单元测试。相反,我们使用一个简单的 Python 脚本 StatsPython.py ,它扩展了我们在前一章中使用的基本脚本。我们使用别名Stats作为一种简单的权宜之计:

import StatsPythonPyBind as Stats
#import StatsPythonBoost as Stats

这使得我们可以轻松地在增强模式之间切换。Python 扩展模块和 PyBind 扩展模块。这并不是作为一种通用方法提出的,它只是为了方便测试这里的函数和类。

脚本本身定义了执行底层 StatsLib 功能的函数。例如,它还允许我们对TTest类进行简单的并行测试。清单 8-10 显示了功能run_statistical_tests2

-

Listing 8-10A simple function to compare the results from two t-tests

img/519150_1_En_8_Figm_HTML.jpg

在清单 8-10 中,该函数将两个熊猫数据框对象(从 csv 文件加载的简单数据集)作为输入,并将它们转换为list s,这是我们的 Python 接口 StatsLib 所期望的类型。第一个调用使用过程接口。第二个相同的调用构造了我们声明的TTest类的一个实例,并调用函数PerformResults。毫不奇怪,这两种方法产生了相同的结果。

表演

尝试连接 C++ 和 Python 的原因之一是 C++ 代码可能带来的性能提升。为此,我们编写了一个小脚本, PerformanceTest.py 。我们想测试均值和(样本)标准差函数的性能。我们希望对 500,000 个项目的 Python vs. PyBind computing MeanStdDev也这样做。

从 Python 的角度来看,我们有两种方法。首先,我们定义函数meanvariance,stddev。这些实现只使用基本的 Python 功能。我们还定义了相同的函数,这次使用的是 Python 统计库。这允许我们有两个不同的基线。

从 C++ 方面来说,我们对 PyBind 模块定义做了一个小的调整,这样我们就可以从 StatsLib 中公开函数MeanStandardDeviation。对于Mean函数来说,这很容易做到。这些函数存在于Stats::DescriptiveStatistics名称空间中,并在静态库中定义。使用 PyBind 包装器 StatsPythonPyBind,我们需要做的就是将清单 8-11 中所示的描述添加到模块定义中。

-

Listing 8-11Enhancing the module definition with additional C++ functions

img/519150_1_En_8_Fign_HTML.png

在清单 8-11 中,我们添加了函数"Mean",提供了 C++ 实现的地址,并添加了文档字符串。

StandardDeviation函数稍微复杂一些。底层 C++ 函数有两个参数,一个是std::vector<double>,另一个是VarianceType的枚举。如果我们只是将函数地址传递给模块定义,我们将从 Python 得到一个运行时错误,因为函数需要两个参数。为了解决这个问题,我们需要扩展代码。此时我们有一个选择。我们可以编写一个小的包装函数来提供硬编码的VarianceType参数,或者我们可以公开VarianceType枚举。我们将研究这两种方法。

首先,我们来看看如何编写一个小的包装函数。清单 8-12 展示了这种方法。

-

Listing 8-12Wrapper for the underlying StandardDeviation function

img/519150_1_En_8_Figo_HTML.png

用硬编码的参数包装函数不太理想,但很简单。在模块定义中,我们添加了清单 8-13 中所示的声明。

-

Listing 8-13Definition of the SampleStandardDeviation wrapper function

img/519150_1_En_8_Figp_HTML.png

在清单 8-13 中,我们使用名称“StdDevS”来反映我们请求样本标准偏差的事实。现在我们可以在性能测试中使用这个函数。

编写包装函数的另一种方法是向 Python 公开VarianceType枚举。如果我们这样做,那么我们可以如下调用函数:

>>> data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> Stats.StdDev(data, Stats.VarianceType.Sample)
3.0276503540974917
>>> Stats.StdDev(data, Stats.VarianceType.Population)
2.8722813232690143

为了在代码中实现这一点,我们需要做两个小的改动。首先,我们描述模块中的枚举。这显示在清单 8-14 中。

-

Listing 8-14Defining the enumeration for VarianceType

img/519150_1_En_8_Figq_HTML.png

在清单 8-14 中,我们使用 PyBind py::enum_类来定义枚举VarianceType并给它命名。注意,在这种情况下,我们已经将enum附加到了模块上下文(py::enum_函数中的m参数),因为它不是类的一部分。然后,我们为相应的值添加适当的字符串。PyBind 文档中给出了对py::enum_更详细的描述。我们还需要对函数在模块中的定义方式做一个小小的修改,以反映它需要两个参数的事实。这显示在清单 8-15 中。

-

Listing 8-15Defining additional arguments for the StdDev function

img/519150_1_En_8_Figr_HTML.jpg

在清单 8-15 中,我们在函数定义中添加了两个py::arg结构。这类似于我们处理GetDescriptiveStatistics函数的第二个可选参数的方式。代码编译时没有警告或错误。我们可以使用 Python 交互式 shell 测试它是否按预期工作,如下所示:

>>> Stats.VarianceType.__members__
{'Sample': <VarianceType.Sample: 0>, 'Population': <VarianceType.Population: 1>}

有了这些修改,我们就可以回到性能测试了。 PerformanceTest.py 脚本很简单。我们导入所需的库,包括 StatsPythonPyBind。我们在 Python 中定义了两个版本的meanstddev。一个版本不使用统计库,第二个版本使用。这只是方便了 Python 函数和我们的库函数之间的比较。我们添加了一个简单的测试函数,它使用随机数据并返回带有计时信息的meanstddev

这些是我们获得的结果(运行纯 Python 函数,而不是 Python 统计库,我们有理由期望后者执行得更快):

Running benchmarks with COUNT = 500000
[mean(x)] (Python implementation) took 0.005 seconds
[stdev(x)] (Python implementation) took 3.003 seconds
[Mean(x)] (C++ implementation) took 0.182 seconds
[StdDevS(x)] (C++ implementation) took 0.183 seconds

Python 函数mean(x)比原生 C++ 函数快大约两个数量级。将 C++ 代码改为使用for-loop而不是std::accumulate并没有明显的区别。调查 C++ 端的延迟是由于转换层还是简单的不必要的向量复制,这可能是很有趣的。然而,原生 C++ StdDev函数比任何一种 Python 变体都要快得多。

统计服务

在 StatsPython 项目中,有一个启动小 Flask 应用程序的脚本 StatsService.py 。Flask 应用程序是一个 web 服务的简单演示。它非常有限,仅允许用户计算汇总数据 t-test。主页面如图 8-1 所示。

img/519150_1_En_8_Fig1_HTML.png

图 8-1

统计服务主页

主页包含一个简单的表单,允许用户输入汇总数据 t-test 的参数。按下提交按钮后,我们计算需要的值并返回,如图 8-2 所示。

img/519150_1_En_8_Fig2_HTML.png

图 8-2

汇总数据 t 检验的结果

例如,要运行服务,请在 VSCode 中打开 StatsPython 项目。在终端中,键入

> py .\StatsService.py

这将在端口 5000 上启动 Flask 服务。在你的浏览器地址栏中,进入 http://localhost:5000/ 。这指向摘要数据 T-Test 页面,这是该应用程序的主页。填写所需的详细信息,然后按提交。使用来自 StatsPythonPyBind 模块的底层TTest类,结果按预期返回。

除了启动和运行所需的少量代码之外,值得强调的是我们在多语言开发基础设施方面取得的成就。我们已经有了一个基础设施,允许我们开发和修改本机 C++ 代码,将其构建到一个库中,将该库合并到一个 Python 模块中,并在 Python web 服务中使用该功能。这种灵活性在开发软件系统时很有价值。

摘要

在本章中,我们已经使用 Boost 提供的框架构建了 Python 模块。Python 和 PyBind。这两个模块以相似的方式展示了底层统计函数库的功能。我们已经看到这两个框架在类型转换和错误处理方面都为我们做了大量的工作。此外,这两个框架都允许我们向 Python 公开原生 C++ 类。在本章的最后,我们对比了底层 C++ 函数调用和 Python 函数调用的性能。性能增强的潜力是将 C++ 连接到 Python 的一个明显原因。然而,将 C++ 连接到 Python 的一个同样令人信服的原因(如果不是更多的话)是,它让我们可以访问各种不同的 Python 库,涵盖了从机器学习(例如 NumPy 和 Pandas)到 web 服务(例如 Django 和 Flask)等等。正如我们所见,在开发松散耦合的软件系统时,能够以最小的努力向 Python 公开用 C++ 编写的功能为您提供了一个有用的额外架构选择。

额外资源

下面的链接提供了本章所涉及主题的更深入的内容。

练习

本节中的练习处理公开与前面相同的功能,但这次是通过 Boost。Python 模块和 PyBind 模块。

以下练习使用了 StatsPythonBoost 项目:

1)在 StatsPythonBoost 中,为 z-test 函数添加过程包装器。这些函数应该与 t-test 函数几乎相同。不需要额外的转换功能。

  • StatisticalTests.h 中,添加以下三个函数的声明:

    boost::python::dict SummaryDataZTest(const boost::python::object& mu0, const boost::python::object& mean, const boost::python::object& sd, const boost::python::object& n);
    
    boost::python::dict OneSampleZTest(const boost::python::object& mu0, const boost::python::list& x1);
    
    boost::python::dict TwoSampleZTest(const boost::python::list& x1, const boost::python::list& x2);
    
    
  • StatisticalTests.cpp 中,添加这些函数的实现。遵循 t-test 包装函数的代码。

  • 在 *module.cpp 中,*向模块BOOST_PYTHON_MODULE(StatsPythonBoost) {}添加三个新函数

  • 重新构建 StatsPythonBoost 后,在 VSCode 中打开 StatsPython 项目。打开 StatsPython.py 脚本。添加函数以使用我们之前使用的数据测试 z 测试函数。例如,我们可以添加以下函数:

    def one_sample_ztest() -> None:
        """ Perform a one-sample z-test """
        try:
            data: list = [3, 7, 11, 0, 7, 0, 4, 5, 6, 2]
            results = Stats.OneSampleZTest(3.0, data)
            print_results(results, "One-sample z-test.")
        except Exception as inst:
            report_exception(inst)
    
    

2)在 StatsPythonBoost 项目中,添加一个MovingAverage函数。

  • Functions.h 中添加以下声明:

    boost::python::list MovingAverage(const boost::python::list& dates, const boost::python::list& observations, const boost::python::object& window);
    
    
  • Functions.cpp 中:

    • #include "TimeSeries.h"添加到文件的顶部。

    • 添加实现:该函数接受三个非可选参数:一个日期列表、一个观察值列表和一个窗口大小。

    • 使用现有的转换函数转换输入,并将它们传递给TimeSeries类的构造函数。

    • 使用Conversion::to_list函数返回结果。

  • module.cpp 中,添加新函数:

    def("MovingAverage", API::MovingAverage, "Compute a simple moving average of size = window.");
    
    
  • 构建 StatsPythonBoost。它的构建应该没有警告和错误。您应该能够交互地测试MovingAverage函数,修改我们之前使用的脚本。

  • 在 VSCode 中打开 StatsPython 项目。打开 StatsPython.py 脚本。添加一个函数来测试移动平均线,包括异常处理。运行脚本,并根据需要进行调试。

3)在 StatsPythonBoost 项目中,添加一个TimeSeries类,该类包装了原生 C++ TimeSeries类并计算一个简单的移动平均值。

所需的步骤如下:

  • 向项目添加一个 TimeSeries.h 和一个 TimeSeries.cpp 文件。它们将分别包含包装类的定义和实现。

  • TimeSeries.h 中,添加类声明。例如:

    namespace API
    {
        namespace TS
        {
            // TimeSeries wrapper class
            class TimeSeries final
            {
            public:
    // Constructor, destructor, assignment operator and MovingAverage function
    
            private:
                Stats::TimeSeries m_ts;
            };
        }
    }
    
    
  • 在 *TimeSeries.cpp 中,*添加类实现。构造函数将boost::python::list参数转换成合适的std::vector类型。MovingAverage函数提取窗口大小参数,并将调用转发给m_ts成员。使用Conversion::to_list()函数返回结果。

  • module.cpp 中,添加包含文件,并将类声明添加到BOOST_PYTHON_MODULE(StatsPythonBoost)中,如下:

    // Declare the TimeSeries class
    class_<API::TS::TimeSeries>("TimeSeries",
    init<const list&, const list&>("Construct a time series from a vector of dates and observations."))
    .def("MovingAverage", &API::TS::TimeSeries::MovingAverage,
            "Compute a simple moving average of size = window.")
        ;
    
    
  • 重新构建 StatsPythonBoost 后,在 VSCode 中打开 StatsPython 项目。打开 StatsPython.py 脚本。添加一个函数来测试移动平均线,包括异常处理。运行脚本,必要时进行调试。

以下练习使用 StatsPythonPyBind 项目:

4)为 z 测试函数添加过程包装器。这些函数应该与 t-test 函数几乎相同。不需要额外的转换功能。

  • 在 *module.cpp 中,*添加三个函数的声明/定义。

  • 在模块定义中,为这三个函数添加条目。遵循 t-test 包装函数的代码。

  • 在重新构建 StatsPythonPyBind 项目后,在 VSCode 中打开 StatsPython 项目。打开 StatsPython.py 脚本。添加函数以使用我们之前使用的数据测试 z 测试函数。

5)给PYBIND11_MODULE添加一个新的类别ZTest。例如,遵循TTest类的定义:

py::class_<Stats::ZTest>(m, "ZTest")
    .def(py::init<double, double, double, double>(), "...")
    .def(py::init<double, const std::vector<double>& >(), "...")
    .def(py::init<const std::vector<double>&, const std::vector<double>& >(), "...")
    .def("Perform", &Stats::ZTest::Perform, "...")
    .def("Results", &Stats::ZTest::Results, "...")
    .def("__repr__", [](const Stats::ZTest& a) {
                return "<example.ZTest>";
            }
    );

注意,在这种情况下,不需要单独的包装器。我们可以简单地引用底层的原生 C++ 类。

  • 在重新构建 StatsPythonPyBind 项目后,在 VSCode 中打开 StatsPython 项目。打开 StatsPython.py 脚本。添加函数以使用我们之前使用的数据测试 z 测试函数。我们可以扩展之前用于测试单样本 z 测试的函数,以测试过程包装器和类,如下所示:
def one_sample_ztest() -> None:
    """ Perform a one-sample z-test """
    try:
        data: list = [3, 7, 11, 0, 7, 0, 4, 5, 6, 2]
        results = Stats.OneSampleZTest(3.0, data)
        print_results(results, "One-sample z-test.")

        z: Stats.ZTest = Stats.ZTest(3.0, data)
        z.Perform()
        print_results(z.Results(), "One-sample z-test.(class)")

    except Exception as inst:
        report_exception(inst)

两次调用的结果输出应该是相同的。

6)在 StatsPythonPyBind 项目中,添加一个MovingAverage函数。

  • 在 *module.cpp 中,*增加#include "TimeSeries.h"

  • 在 *module.cpp 中,*添加包装函数的声明/定义。

  • module.cpp 中,将MovingAverage函数的定义添加到PYBIND11_MODULE公开的函数列表中。

  • 在重新构建 StatsPythonPyBind 项目后,在 VSCode 中打开 StatsPython 项目。打开 StatsPython.py 脚本。添加一个函数来测试移动平均线,包括异常处理。运行脚本,必要时进行调试。

std::vector<double> MovingAverage(const std::vector<long>& dates, const std::vector<double>& observations, int window)
{
        Stats::TimeSeries ts(dates, observations);
        const auto results = ts.MovingAverage(window);
        return results;
}

7)暴露原生 C++ TimeSeries类和简单移动平均函数。

  • module.cpp 中,添加包含文件,并添加类声明。该类定义将类似于我们之前添加到 StatsPythonBoost 项目中的类定义。
py::class_<Stats::TimeSeries>(m, "TimeSeries")
    .def(py::init<const std::vector<long>&, const std::vector<double>&>(),
        "Construct a time series from a vector of dates and observations.")
    .def("MovingAverage", &Stats::TimeSeries::MovingAverage, "Compute a simple moving average of size = window.")
    .def("__repr__",
        [](const Stats::TimeSeries& a) {
            return "<TimeSeries> containing: " + to_string(a);
        }
);

为了正确地添加__repr__方法,我们需要修改底层的类定义以允许访问内部,或者编写一个额外的to_string()方法。这是最后一个练习。

  • 在重新构建 StatsPythonPyBind 项目后,在 VSCode 中打开 StatsPython 项目。打开 StatsPython.py 脚本。添加一个函数来测试移动平均线,包括异常处理。运行脚本,必要时进行调试。

值得强调的是,与通过 CPython 或 Boost 公开包装器所需的工作量相比,使用 PyBind 公开ZTest类和TimeSeries类非常简单。Python 包装器。

Footnotes 1

PyBind 文档中的第七章函数( https://pybind11.readthedocs.io/_/downloads/en/latest/pdf/ )提供了完整的细节。

 

九、总结

在本书中,我们的目标是开发将 C++ 代码库(虽然简单,但有点做作)连接到用其他语言编写的客户端软件的组件,特别是 C#、R 和 Python。目的是使 C++ 库中的功能可用,并允许从其他客户端语言访问该功能。这是我们已经完成的。在这样做的时候,我们已经涉及了相当多的领域。

我们从构建一个 C++ 统计函数库开始。这构成了我们想要向客户展示的基础。原因是这很容易理解,但比一个玩具例子更完整。在开发连接到其他语言的包装器组件时,它有足够的特性来说明现实世界中的问题。在整本书中,我们让源代码驱动我们想要公开的内容,虽然这在某种程度上限制了覆盖范围,但也更易于管理。

在这个 C++ 代码基础之上,我们构建了包装器组件,允许我们向不同的语言公开 C++ 功能。首先,我们构建了一个 C++/CLI 程序集。我们看到它在许多不同的环境中是多么容易使用。我们在一个简单的控制台客户端中测试了该功能——测试了我们调用的函数以及该组件如何与其他 C# 库(在本例中为 Accord)进行互操作。网)。我们还通过 Excel-DNA 毫不费力地将组件连接到 Excel。

在此之后,我们构建了一个 R 包,它使用 Rcpp 将 C++ 代码库连接到 R。和以前一样,我们练习了基本功能,但也看了一下如何将 StatsR 组件与其他 R 包一起使用,特别是tidyverseggplot,benchmark。最后,我们构建了一个闪亮的小应用程序来演示我们的组件如何与其他 R 包交互。我们看到了 StatsR 包可以在广阔的 R 宇宙中的任何地方使用。在这个过程中,我们建立了一个开发基础设施,它由一个 IDE (CodeBlocks)和一个 IDE 组成,IDE 用于使用 R 所需的编译器来构建 C++ 代码,IDE 用于编写 Rcpp 包(RStudio),我们可以使用它来开发和构建包。

最后,我们还构建了 Python 扩展模块:确切地说是三个。我们看到了使用低级 CPython 方法的潜在缺陷,然后考虑使用两种 Boost。Python 和 PyBind 作为连接 C++ 和 Python 的框架。我们看到了这两种框架如何促进包装组件的开发。我们还看到,潜在的性能提升(不能保证)只是将 C++ 连接到 Python 的众多可能原因之一。将一个新的组件引入 Python 世界,与 NumPy 和 Pandas 等库一起无缝运行,这也是非常重要的。

总的来说,我们已经了解了如何为不同的语言包装组件建立项目。我们已经花了一些时间来研究包装器的设计:将关注点分成功能层、进行调用的部分和类型转换层。我们已经研究了类型转换的细节,以及如何有效地推广它们。在这个过程中,我们触及了许多其他的软件开发主题:一些与代码相关的,比如异常处理;其他的与开发过程相关,比如测试和调试。除了简单地构建组件,我们还建立了一个简化(多语言)开发过程的工具基础设施。

另一方面,在限制我们自己公开有限的 C++ 库中的底层功能时,我们忽略了许多重要的方面。我们还没有触及原生 C++ 库中的线程和并发性,以及如何将其暴露给不同的客户端语言。从实际组件的角度来看,我们忽略了许多方面。在 C# 中,我们还没有覆盖委托;在 R 中,我们没有涉及扩展模块;在 Python 中,我们仅仅触及了 PyBind 的皮毛。所有这些都需要一本书。我们在这里提供的是未来发展的一些起点。

在更一般的层面上,目的是扩展开发软件时可用的架构选择。在第二章中,我们看到了在 Windows 应用程序中直接包含组件的一些限制(链接到组件dll )。我们已经证明了一个可行的替代方案是开发可以在多种上下文(Windows 应用程序、web 应用程序)中使用的包装器组件(程序集、包和模块),并且来自不同的语言,C#、R 和 Python。这导致了一个更加松散耦合的软件系统。组件本身可以提供完全异构的服务,但是它们可以互操作,因为它们参与了底层框架,无论它是。NET 通过 C++/CLI,R 通过 Rcpp,或者 Python。

第一部分:基础

第二部分:C++/CLI 和 .NET

第三部分:R 和 Rcpp

第四部分:Python