在 Visual Studio 中创建适用于 Python 的 C++ 扩展

在本文中,你将生成一个适用于 CPython 的 C++ 扩展模块,以便计算双曲正切并从 Python 代码中调用它。 首先使用 Python 实现此例程,以演示使用 C++ 实现此相同例程时可获得的相对性能提升。

使用 C++(或 C)编写的代码模块通常会用于扩展 Python 解释器的功能。 主要有以下 3 种扩展模块:

  • 加速器模块:实现加速性能。 由于 Python 是一种解释型语言,因此可用 C++ 来编写加速器模块,从而实现更高的性能。
  • 包装器模块:向 Python 代码公开现有 C/C++ 接口,或公开易于通过 Python 来使用的更“Python 化”的 API。
  • 低级系统访问模块:创建系统访问模块以访问 CPython 运行时、操作系统或基础硬件的较低级别功能。

本文演示了使 C++ 扩展模块可用于 Python 的两种方法:

  • 使用标准 CPython 扩展,如 Python 文档中所述。
  • 使用 PyBind11,由于其简单易用,因此推荐用于 C++11。 若要确保兼容性,请务必使用较新版本的 Python。

GitHub 上的 python-samples-vs-cpp-extension 提供了本演练的完整示例。

先决条件

  • Visual Studio 2017 或更高版本,且已安装 Python 开发工作负荷。 该工作负荷包括 Python 本机开发工具,而它添加了本机扩展所需的 C++ 工作负荷和工具集。

    Screenshot of a list of Python development options, highlighting the Python native development tools option.

    若要详细了解安装选项,请参阅安装对 Visual Studio 的 Python 支持

    注意

    在安装数据科学和分析应用程序工作负载时,会默认安装 Python 和 Python 本机开发工具选项。

  • 如果要单独安装 Python,则请务必在 Python 安装程序的高级选项下选择下载调试符号。 若要在 Python 代码和本机代码之间使用混合模式调试,则必须使用此选项。

创建 Python 应用程序

按以下步骤创建 Python 应用程序。

  1. 要在 Visual Studio 中创建新 Python 项目,请选择文件>新建>项目

  2. 创建新项目对话框中,搜索 python。 选择 Python 应用程序模板,然后选择下一步

  3. 输入项目名称位置,然后选择创建

    Visual Studio 随即创建新项目。 此项目随即在解决方案资源管理器中打开,而项目文件 (.py) 则会在代码编辑器中打开。

  4. .py 文件中粘贴以下代码。 要体验某些 Python 编辑功能,请尝试手动输入代码。

    此代码在不使用数学库的情况下会计算双曲正切,而你会使用它并通过 Python 本机扩展来实现后续加速。

    提示

    在用 C++ 重写之前,请先用纯 Python 编写代码。 如此一来,便可更轻松地通过检查来确保本机 Python 代码正确无误。

    from random import random
    from time import perf_counter
    
    # Change the value of COUNT according to the speed of your computer.
    # The value should enable the benchmark to complete in approximately 2 seconds.
    COUNT = 500000
    DATA = [(random() - 0.5) * 3 for _ in range(COUNT)]
    
    e = 2.7182818284590452353602874713527
    
    def sinh(x):
        return (1 - (e ** (-2 * x))) / (2 * (e ** -x))
    
    def cosh(x):
        return (1 + (e ** (-2 * x))) / (2 * (e ** -x))
    
    def tanh(x):
        tanh_x = sinh(x) / cosh(x)
        return tanh_x
    
    def test(fn, name):
        start = perf_counter()
        result = fn(DATA)
        duration = perf_counter() - start
        print('{} took {:.3f} seconds\n\n'.format(name, duration))
    
        for d in result:
            assert -1 <= d <= 1, " incorrect values"
    
    if __name__ == "__main__":
        print('Running benchmarks with COUNT = {}'.format(COUNT))
    
        test(lambda d: [tanh(x) for x in d], '[tanh(x) for x in d] (Python implementation)')
    
  5. 通过选择调试>启动但不调试来运行程序,或按键盘快捷方式 Ctrl+F5

    随即打开一个命令窗口以显示程序输出。

  6. 在此输出中,请注意为基准流程报告的时间量。

    对于本演练,基准流程约需 2 秒。

  7. 按需调整代码中 COUNT 变量的值,以使基准测试能在约 2 秒内在计算机上完成。

  8. 再次运行程序,并确认修改后的 COUNT 值会在约 2 秒内生成基准。

提示

运行基准检验时,请始终使用调试>启动但不调试选项。 此方法有助于避免在 Visual Studio 调试器中运行代码时可能产生的开销。

创建核心 C++ 项目

按以下步骤创建两个相同的 C++ 项目:superfastcodesuperfastcode2。 稍后,在每个项目中使用单独的方法以将 C++ 代码公开给 Python。

  1. 解决方案资源管理器中,右键单击解决方案名称,然后选择添加>新建项目

    一个 Visual Studio 解决方案可同时包含 Python 和 C++ 项目(这是将 Visual Studio 用于 Python 开发的好处之一)。

  2. 添加新项目对话框中,将语言筛选器设为 C++,并在搜索框中输入

  3. 在项目模板结果列表中,选择空项目,然后选择下一步

  4. 配置新项目对话框中,输入项目名称

    • 对于第一个项目,请输入名称 superfastcode
    • 对于第二个项目,请输入名称 superfastcode2
  5. 选择创建

请务必重复执行这些步骤,并创建两个项目。

提示

在 Visual Studio 中已安装 Python 本机开发工具时,可使用替代方法。 可从 Python 扩展模块模板开始,而该模板预先完成了本文所述的众多步骤。

对于本文中的演练,从空项目开始有助于逐步演示如何生成扩展模块。 了解此流程后,可在编写自己的扩展时利用备用模板来节省时间。

将 C++ 文件添加到项目

接下来,向每个项目添加一个 C++ 文件。

  1. 解决方案资源管理器中,右键单击源文件节点,然后选择添加>新建项目

  2. 在文件模板列表中,选择 C++ 文件(.cpp)

  3. 将此文件的名称输入为 module.cpp,然后选择添加

    重要

    请确保文件名包含 .cpp 扩展名。 Visual Studio 会查找具有 .cpp 扩展名的文件,以便启用 C++ 项目属性页面的显示。

  4. 在工具栏上,展开配置下拉菜单并选择目标配置类型:

    Screenshot that shows how to set the target configuration type for the C++ project in Visual Studio.

    • 对于 64 位 Python 运行时,请激活 x64 配置。
    • 对于 32 位 Python 运行时,请激活 Win32 配置。

请务必对这两个项目重复执行这些步骤。

配置项目属性

将代码添加到新的 C++ 文件之前,请为每个 C++ 模块项目配置这些属性,并测试这些配置以确保一切均可正常工作。

需为每个模块的调试发布生成配置设置项目属性。

  1. 解决方案资源管理器中,右键单击 C++ 模块项目(superfastcodesuperfastcode2),然后选择属性

  2. 为此模块的调试版本配置属性,然后为发布版本配置相同的属性:

    在项目属性页面对话框的顶部,配置以下文件配置选项:

    Screenshot that shows how to set the project build and platform options for the C++ file in Visual Studio.

    1. 对于配置,请选择调试发布。 (可能会发现这些选项具有活动前缀。)

    2. 对于平台,根据你在上一步骤中做出的选择来选择活动(x64)活动(Win32)

      注意

      创建自己的项目时,需根据特定场景要求分别配置调试发布配置。 在本练习中,将这些配置设置为使用 CPython 的发布版本。 此配置禁用 C++ 运行时的一些调试功能,包括断言。 要使用 CPython 调试二进制文件 (python_d.exe),需要完成不同的设置。

    3. 按下表所述方法设置其他项目属性。

      若要更改属性值,请在属性字段中输入值。 对于某些字段,可选择当前值以展开选项下拉菜单或打开某一对话框来帮助定义该值。

      更新某一选项卡上的值后,请在切换到其他选项卡之前选择应用。此操作有助于确保更改保持不变。

      选项卡和部分 属性 Value
      配置属性>常规 目标名称 指定要在 from...import 语句中从 Python 引用的模块的名称,如 superfastcode。 定义 Python 的模块时,请在 C++ 代码中使用相同的名称。 若要将项目的名称用作模块名称,请保留默认值 $<ProjectName>。 对于 python_d.exe,在名称末尾添加 _d
      配置类型 动态库(.dll)
      配置属性>高级 目标文件扩展名 .pyd(Python 扩展模块)
      C/C++>常规 附加包含目录 根据相应的安装添加 Python include 文件夹(例如 c:\Python36\include)。
      C/C++>预处理器 预处理器定义 如果已存在,则请将 _DEBUG 值更改为 NDEBUG,从而匹配非调试版本的 CPython。 使用 python_d.exe 时,请将此值保持不变。
      C/C++>代码生成 运行时库 多线程 DLL (/MD),以匹配发行(非调试)版本的 CPython。 使用 python_d.exe 时,请将此值保留为多线程调试 DLL (/MDd)
      基本运行时检查 默认值
      链接器>常规 附加库目录 根据相应的安装添加包含 .lib 文件的 Python libs 文件夹(例如 c:\Python36\libs)。 务必指向包含 .lib 文件的 libs 文件夹,而包含 .py 文件的 Lib 文件夹。

      重要

      如果 C/C++ 选项卡未显示为项目属性的一个选项,则该项目不含 Visual Studio 标识为 C/C++ 源文件的代码文件。 如果创建的源文件不含 .c.cpp 文件扩展名,则可能出现这种情况。

      如果在创建 C++ 文件时不慎输入 module.coo 而不是 module.cpp,Visual Studio 则会创建该文件,但不会将文件类型设为 C/C+ 编译器。 此文件类型是在项目属性对话框中激活 C/C++ 属性选项卡的存在所必需的文件类型。 即使用 .cpp 文件扩展名重命名此代码文件,也会出现错误标识。

      若要正确设置代码文件类型,请在解决方案资源管理器中右键单击该代码文件,然后选择属性。 对于项目类型,请选择 C/C++ 编译器

    4. 更新所有属性后,选择确定

    为另一生成配置重复执行上述步骤。

  3. 测试当前配置。 对这两个 C++ 项目的调试发布版本重复执行以下步骤。

    1. 在 Visual Studio 工具栏上,将生成配置设为调试发布

      Screenshot that shows how to set the build configuration for the C++ project in Visual Studio.

    2. 解决方案资源管理器中,右键单击 C++ 项目,然后选择生成

      .pyd 文件位于调试发布下的解决方案文件夹中,而非位于 C++ 项目文件夹自身。

添加代码和测试配置

现在,你已准备好将代码添加到 C++ 文件并测试发布版本。

  1. 对于 superfastcode C++ 项目,请在代码编辑器中打开 module.cpp 文件。

  2. module.cpp 文件中,粘贴以下代码:

    #include <Windows.h>
    #include <cmath>
    
    const double e = 2.7182818284590452353602874713527;
    
    double sinh_impl(double x) {
        return (1 - pow(e, (-2 * x))) / (2 * pow(e, -x));
    }
    
    double cosh_impl(double x) {
        return (1 + pow(e, (-2 * x))) / (2 * pow(e, -x));
    }
    
    double tanh_impl(double x) {
        return sinh_impl(x) / cosh_impl(x);
    }
    
  3. 保存所做更改。

  4. 生成 C++ 项目的发布配置,从而确认此代码正确无误。

重复执行这些步骤,以便将代码添加到 superfastcode2 项目的 C++ 文件并测试发布版本。

将 C++ 项目转换为 Python 扩展

若要使 C++ DLL 成为适用于 Python 的扩展,首先应修改导出的方法以便与 Python 类型交互。 然后,添加一个可用于导出该模块的函数以及该模块的方法的定义。

以下各节演示如何使用 CPython 扩展和 PyBind11 来创建扩展。 superfasctcode 项目使用 CPython 扩展,而 superfasctcode2 项目则会实现 PyBind11。

使用 CPython 扩展

有关本节中所示代码的详细信息,请参阅 Python/C API 参考手册,尤其是模块对象页面。 查看参考内容时,请务必在右上方的下拉列表中选择你的 Python 版本。

  1. 对于 superfastcode C++ 项目,请在代码编辑器中打开 module.cpp 文件。

  2. module.cpp 文件的顶部添加一个语句以包含 Python.h 头文件:

    #include <Python.h>
    
  3. 替换 tanh_impl 方法代码以接受并返回 Python 类型(即 PyObject*):

    PyObject* tanh_impl(PyObject* /* unused module reference */, PyObject* o) {
        double x = PyFloat_AsDouble(o);
        double tanh_x = sinh_impl(x) / cosh_impl(x);
        return PyFloat_FromDouble(tanh_x);
    }
    
  4. 在此文件的末尾,添加一个结构以定义如何将 C++ tanh_impl 函数提供给 Python:

    static PyMethodDef superfastcode_methods[] = {
        // The first property is the name exposed to Python, fast_tanh
        // The second is the C++ function with the implementation
        // METH_O means it takes a single PyObject argument
        { "fast_tanh", (PyCFunction)tanh_impl, METH_O, nullptr },
    
        // Terminate the array with an object containing nulls
        { nullptr, nullptr, 0, nullptr }
    };
    
  5. 添加另一结构以定义如何在 Python 代码中引用该模块(尤其是在使用 from...import 语句时)。

    此代码中要导入的名称应与配置属性>常规>目标名称中的项目属性值匹配。

    在以下示例中,"superfastcode" 名称表示可在 Python 中使用 from superfastcode import fast_tanh 语句,因为 fast_tanh 是在 superfastcode_methods 中定义的。 C++ 项目的内部文件名称(如 module.cpp)是无关紧要的。

    static PyModuleDef superfastcode_module = {
        PyModuleDef_HEAD_INIT,
        "superfastcode",                        // Module name to use with Python import statements
        "Provides some functions, but faster",  // Module description
        0,
        superfastcode_methods                   // Structure that defines the methods of the module
    };
    
  6. 添加 Python 加载此模块时所调用的方法。 方法名称必须为 PyInit_<module-name>,其中 <module-name> 与 C++ 项目的配置属性>常规>目标名称属性完全匹配。 换言之,此方法名称与项目生成的 .pyd 的文件名相匹配。

    PyMODINIT_FUNC PyInit_superfastcode() {
        return PyModule_Create(&superfastcode_module);
    }
    
  7. 生成 C++ 项目并验证代码。 如果出现错误,请参阅排查编译错误一节。

使用 PyBind11

如果已为 superfastcode 完成上一节中提及的步骤,则可能会注意到该练习需要样本代码来创建 C++ CPython 扩展的模块结构。 在本练习中,你发现 PyBind11 可简化编码流程。 在 C++ 头文件中使用宏可实现同一结果,但代码要少得多。 但是,需执行额外步骤来确保 Visual Studio 可找到 PyBind11 库并包含文件。 有关本部分中的代码的详细信息,请参阅 PyBind11 基础知识

安装 PyBind11

第一步是用项目配置来安装 PyBind11。 在本练习中,你将使用开发人员 PowerShell 窗口。

  1. 打开工具>命令行>开发人员 PowerShell 窗口。

  2. 开发人员 PowerShell 窗口中,使用 pip 命令 pip install pybind11py -m pip install pybind11 来安装 PyBind11。

    Visual Studio 会安装 PyBind11 及其依赖包。

将 PyBind11 路径添加到项目

安装 PyBind11 后,需将 PyBind11 路径添加到此项目的其他包含目录属性。

  1. 开发人员 PowerShell 中,运行命令 python -m pybind11 --includespy -m pybind11 --includes

    此操作会输出需添加到项目属性的 PyBind11 路径的列表。

  2. 在此窗口中突出显示路径列表,然后在窗口工具栏上选择复制(双页)。

    Screenshot that shows how to highlight and copy the list of paths from the Developer PowerShell window in Visual Studio.

    串联后路径的列表随即会添加到剪贴板。

  3. 解决方案资源管理器中,右键单击 superfastcode2 项目,然后选择属性

  4. 属性页面对话框顶部,为配置字段选择发布。 (可能会发现此选项具有活动前缀。)

  5. 在此对话框中的 C/C++>常规选项卡中,展开其他包含目录属性的对应下拉菜单,然后选择编辑

  6. 在弹出对话框中,添加已复制路径的列表:

    对从开发人员 PowerShell 窗口复制的串联列表中的每个路径重复执行以下步骤:

    1. 在弹出对话框工具栏上,选择新建行(带加号的文件夹)。

      Screenshot that shows how to add a PyBind11 path to the Additional Include Directories property.

      Visual Studio 会在路径列表顶部添加一个空行,并将插入光标置于开头。

    2. 将 PyBind11 路径粘贴到空行中。

      此外,还可选择更多选项 (...),并使用弹出文件资源管理器对话框以浏览到该路径位置。

      重要

      • 如果路径包含 -I 前缀,则请从路径中删除此前缀。
      • 若要使 Visual Studio 能识别路径,该路径则需位于单独的行中。

      添加新路径后,Visual Studio 会在计算结果值字段中显示确认后的路径。

  7. 选择确定可退出弹出对话框。

  8. 属性页对话框顶部,将鼠标悬停在其他包含目录属性的值上,然后确认 PyBind11 路径均存在。

  9. 单击确定以应用属性更改。

更新 module.cpp 文件

最后一步是将 PyBind11 头文件和宏代码添加到项目 C++ 文件。

  1. 对于 superfastcode2 C++ 项目,请在代码编辑器中打开 module.cpp 文件。

  2. module.cpp 文件的顶部添加一个语句以包含 pybind11.h 头文件:

    #include <pybind11/pybind11.h>
    
  3. module.cpp 文件的末尾,添加 PYBIND11_MODULE 宏的代码以定义 C++ 函数的入口点:

    namespace py = pybind11;
    
    PYBIND11_MODULE(superfastcode2, m) {
        m.def("fast_tanh2", &tanh_impl, R"pbdoc(
            Compute a hyperbolic tangent of a single argument expressed in radians.
        )pbdoc");
    
    #ifdef VERSION_INFO
        m.attr("__version__") = VERSION_INFO;
    #else
        m.attr("__version__") = "dev";
    #endif
    }
    
  4. 生成 C++ 项目并验证代码。 如果出现错误,则请参阅下一节排查编译错误

排查编译错误

查看以下各节以了解可能导致 C++ 模块生成失败的潜在问题。

错误:未找到头文件

Visual Studio 将返回错误消息,例如 E1696:无法打开源文件“Python.h”C1083:无法打开包含文件:“Python.h”:无此类文件或目录

此错误表示编译器未找到项目所需的头 (.h) 文件。

  • 对于 superfastcode 项目,请验证 C/C++>常规>其他包含目录项目属性是否包含 Python 安装 include 文件夹的路径。 查看配置项目属性中的步骤。

  • 对于 superfastcode2 项目,请验证同一项目属性是否包含 PyBind11 安装 include 文件夹的路径。 查看向项目添加 PyBind 路径的步骤。

有关访问 Python 安装配置信息的详细信息,请参阅 Python 文档

错误:无法找到 Python 库

Visual Studio 将返回一个错误,表示编译器未找到项目所需的库 (DLL) 文件。

  • 对于 C++ 项目(superfastcodesuperfastcode2),请验证链接器>常规>其他库目录属性是否包含 Python 安装的 libs 文件夹的路径。 查看配置项目属性中的步骤。

有关访问 Python 安装配置信息的详细信息,请参阅 Python 文档

Visual Studio 会报告与项目的目标体系结构配置相关的链接器错误,例如 x64 或 Win32。

  • 对于 C++ 项目(superfastcodesuperfastcode2),请更改目标配置以匹配 Python 安装。 例如,如果 C++ 项目的目标配置为 Win32,但 Python 安装为 64 位,则应将 C++ 项目目标配置更改为 x64。

测试代码和比较结果

将 DLL 结构化为 Python 扩展后,便可从 Python 项目引用它们,导入模块,并使用其方法。

使 DLL 可供 Python 使用

可通过多种方式使 DLL 可供 Python 使用。 请考虑以下两个选项:

如果 Python 项目和 C++ 项目均位于同一解决方案中,则可使用以下方法:

  1. 解决方案资源管理器中,右键单击 Python 项目中的引用节点,然后选择添加引用

    请务必为 Python 项目执行此操作,而不是为 C++ 项目。

  2. 添加引用对话框中,展开项目选项卡。

  3. 同时选中 superfastcodesuperfastcode2 项目的对应复选框,然后选择确定

    Screenshot that shows how to add a reference to the super fast code project in Visual Studio.

另一方法是在 Python 环境中安装 C++ 扩展模块。 此方法可使该模块可供其他 Python 项目使用。 有关详细信息,请参阅 setuptools 项目文档

完成以下步骤,以便在 Python 环境中安装 C++ 扩展模块:

  1. 解决方案资源管理器中,右键单击 C++ 项目,然后选择添加>新建项目

  2. 在文件模板列表中,选择 C++ 文件(.cpp)

  3. 为此文件的名称输入 setup.py,然后选择添加

    请务必输入带 Python (.py) 扩展名的文件名。 虽然使用了 C++ 文件模板,但 Visual Studio 仍会将此文件识别为 Python 代码。

    Visual Studio 会在代码编辑器中打开新文件。

  4. 将以下代码粘贴到新文件中。 选择与扩展方法相对应的代码版本:

    • CPython 扩展superfastcode 项目):

      from setuptools import setup, Extension
      
      sfc_module = Extension('superfastcode', sources = ['module.cpp'])
      
      setup(
          name='superfastcode',
          version='1.0',
          description='Python Package with superfastcode C++ extension',
          ext_modules=[sfc_module]
      )
      
    • PyBind11superfastcode2 项目):

      from setuptools import setup, Extension
      import pybind11
      
      cpp_args = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
      
      sfc_module = Extension(
          'superfastcode2',
          sources=['module.cpp'],
          include_dirs=[pybind11.get_include()],
          language='c++',
          extra_compile_args=cpp_args,
      )
      
      setup(
          name='superfastcode2',
          version='1.0',
          description='Python package with superfastcode2 C++ extension (PyBind11)',
          ext_modules=[sfc_module],
      )
      
  5. 在 C++ 项目中,创建另一个名为 pyproject.toml 的文件,然后粘贴以下代码:

    [build-system]
    requires = ["setuptools", "wheel", "pybind11"]
    build-backend = "setuptools.build_meta"
    

    TOML (.toml) 文件会为配置文件使用 Tom 的“Obvious, Minimal Language”(明显、最少语言)格式。

  6. 若要生成扩展,请在代码窗口选项卡中右键单击 pyproject.toml 文件名,然后选择复制完整路径

    Screenshot that shows how to copy the full path to the py project toml file in Visual Studio.

    使用该路径之前,需从中删除 pyproject.toml 名称。

  7. 解决方案资源管理器中,展开解决方案的 Python 环境节点。

  8. 右键单击活动的 Python 环境(显示为粗体),然后选择管理 Python 包

    随即打开 Python 环境窗格。

    如果已安装必要的包,则会在此窗格中看到它被列出。

    • 在继续之前,请选择包名称旁边的 X 以将其卸载。

    Screenshot that shows how to uninstall a package in the Python Environments pane.

  9. Python 环境窗格的搜索框中,粘贴已复制的路径,然后从路径末尾删除 pyproject.toml 文件名。

    Screenshot that shows how to enter the path in the Python Environments pane to install the extension module.

  10. 选择 Enter 可从已复制路径的位置安装此模块。

    提示

    如果安装因权限错误而失败,请将 --user 参数添加到此命令的末尾,然后重试该安装。

从 Python 调用 DLL

如上一节所述,使 DLL 可供 Python 使用后,即可从 Python 调用 superfastcode.fast_tanh and superfastcode2.fast_tanh2 函数。 然后,可将函数执行情况与 Python 实现进行比较。

按以下步骤从 Python 调用扩展模块 DLL:

  1. 在代码编辑器中打开 Python 项目的 .py 文件。

  2. 在此文件的末尾,添加以下行以调用从 DLL 中导出的方法并显示其输出:

    from superfastcode import fast_tanh
    test(lambda d: [fast_tanh(x) for x in d], '[fast_tanh(x) for x in d] (CPython C++ extension)')
    
    from superfastcode2 import fast_tanh2
    test(lambda d: [fast_tanh2(x) for x in d], '[fast_tanh2(x) for x in d] (PyBind11 C++ extension)')
    
  3. 通过选择调试>启动但不调试来运行 Python 程序,或使用键盘快捷方式 Ctrl+F5

    注意

    如果无法使用启动但不调试命令,则请在解决方案资源管理器中右键单击 Python 项目,然后选择设为启动项目

    当此程序执行时,请注意 C++ 例程的运行速度会比 Python 实现快约 5 到 20 倍。

    典型程序输出的示例如下:

    Running benchmarks with COUNT = 500000
    [tanh(x) for x in d] (Python implementation) took 0.758 seconds
    
    [fast_tanh(x) for x in d] (CPython C++ extension) took 0.076 seconds
    
    [fast_tanh2(x) for x in d] (PyBind11 C++ extension) took 0.204 seconds
    
  4. 尝试增大 COUNT 变量,以使时间差异更为明显。

    C++ 模块调试版本的运行速度也慢于发布版本,因为调试版本的优化程度较低,且包含各种错误检查。 尝试在这些版本配置之间进行切换以便进行比较,但请务必更新之前为发布配置而设置的属性。

解决进程速度和开销问题

在输出中,你可能会注意到 PyBind11 扩展不如 CPython 扩展快,尽管它应比纯 Python 实现更快。 此差异的主要原因在于使用了 METH_O 标志。 此标志不支持多个参数、参数名称或关键字参数。 PyBind11 会生成稍微复杂一些的代码,以便为调用方提供更类似于 Python 的接口。 由于测试代码会调用该函数 500,000 次,因此结果可能会大大增加开销。

可通过将 for 循环移至本机 Python 代码来进一步降低开销。 此方法涉及使用迭代器协议(或用于函数参数的 PyBind11 py::iterable 类型)来处理每个元素。 删除 Python 和 C++ 之间的重复转换是缩短处理序列所需时间的有效方法。

排查导入错误

如果在尝试导入模块时收到一条 ImportError 消息,可以通过以下方式之一来解决此问题:

  • 通过项目引用来生成时,请确保 C++ 项目属性与为 Python 项目激活的 Python 环境匹配。 确认正将相同的文件夹位置用于包含 (.h) 和 (DLL) 文件。

  • 确保输出文件的名称正确,例如 superfastcode.pyd。 不正确的名称或扩展名会阻止导入必要的文件。

  • 如果使用 setup.py 文件来安装模块,则请务必在为 Python 项目激活的 Python 环境中运行 pip 命令。 在解决方案资源管理器中展开项目的活动 Python 环境时,应会看到 C++ 项目的对应条目,例如 superfastcode

调试 C++ 代码

Visual Studio 支持一起调试 Python 和 C++ 代码。 以下步骤演示 superfastcode C++ 项目的调试流程,但此流程与针对 superfastcode2 项目的调试流程相同。

  1. 解决方案资源管理器中,右键单击 Python 项目,然后选择属性

  2. 属性窗格中,选择调试选项卡,然后选择调试>启用本机代码调试选项。

    提示

    启用本机代码调试后,Python 输出窗口可能会在程序完成后立即关闭,而不会暂停和显示按任意键继续提示。 若要在启用本机代码调试后强制暂停并显示提示,请将 -i 参数添加到调试选项卡上的运行>解释器参数字段。此参数会在代码运行后将 Python 解释器设为交互模式。 此程序会等待你选择 Ctrl+Z+Enter 以便关闭该窗口。 另一方法是在 Python 程序的末尾添加 import osos.system("pause") 语句。 此代码会复制初始的暂停提示符。

  3. 选择文件>保存(或 Ctrl+S)以保存属性更改。

  4. 在 Visual Studio 工具栏上,将生成配置设为调试

  5. 由于代码在调试器中运行时通常需要更长时间,因此可能需将 Python 项目 .py 文件中的 COUNT 变量更改为比默认值小约五倍的值。 例如,将该值从 500000 更改为 100000

  6. 在 C++ 代码中,对 tanh_impl 方法的第一行代码设置一个断点。

  7. 选择调试>开始调试或使用键盘快捷方式 F5 来启动调试器。

    调用该断点代码时,调试器将会停止。 如果未命中断点,则请进行检查以确保此配置已设为调试且已保存该项目(启动调试器时,不会自动执行该操作)。

    Screenshot of C++ code that contains a breakpoint in Visual Studio.

  8. 在断点处,可单步执行 C++ 代码、检查变量等。 有关这些功能的详细信息,请参阅同时调试 Python 和 C++

替代方法

可通过多种方式创建 Python 扩展,如下表所示。 本文介绍前两行,即 CPythonPyBind11

方法 年份 代表用户
适用于 CPython 的 C/C++ 扩展模块 1991 标准库
PyBind11(推荐用于 C++) 2015
Cython(推荐用于 C) 2007 geventkivy
HPy 2019
mypyc 2017
ctype 2003 oscrypto
cffi 2013 cryptographypypy
SWIG 1996 crfsuite
Boost.Python 2002
cppyy 2017