如何在 C/C++ 中包装 Python 对象?

2025 年 3 月 25 日 | 阅读 6 分钟

Python 是一种解释型、面向对象的语言,开箱即用地提供了动态类型、反射和高级数据类型等强大功能。Python 丰富的对象模型是其关键优势之一,它能够实现快速应用程序开发和清晰、可读的代码。

然而,对于应用程序中 CPU 或内存密集型的部分,与 C/C++ 等静态编译型语言相比,Python 的动态特性会带来性能开销。因此,将 Python 的易用性用于高级逻辑,并结合 C/C++ 的速度和效率用于性能关键型例程,具有明显的优势。

一种有效的技术可以获得“两全其美”的效果,那就是封装 Python 对象,使其可以直接从 C 或 C++ 代码中使用。这使得可以将关键的应用程序逻辑、类和数据结构保留在高生产力的 Python 中,同时又能从低级的 C/C++ 代码中访问它们。无缝的互操作性开启了许多有趣的可能。

本文探讨了封装 Python 对象以便在 C/C++ 中使用的不同方法。我们将涵盖以下概念:

  • 直接访问 Python C API
  • 使用 Boost.Python 或 pybind11 等辅助库
  • 从 C++ 创建和操作 Python 对象
  • 从 C++ 调用 Python 对象方法
  • 正确管理引用和内存

假设您同时具备 Python 和 C/C++ 的一些经验。读完本文后,您应该会理解在您自己的高性能 C/C++ 应用程序中利用 Python 面向对象特性的技术。让我们开始吧!

目标是提供背景信息,说明为什么在许多领域中从 C/C++ 访问 Python 对象非常有用,并介绍文章其余部分涵盖的核心主题。如果您希望我对引言进行修改或扩展,请告诉我。

访问 Python 对象的步骤

1. 直接访问 Python C API

  • 包含 Python.h 头文件。
  • 调用 Py_Initialize() 来启动 Python 解释器。
  • 使用 PyList_New()、PyDict_SetItem() 等函数来创建和操作 Python 对象。
  • 使用 PyMethod_New() 和 PyEval_CallObject() 来调用对象方法。
  • 手动管理引用计数,每次创建/复制/删除操作都需要。

2. 使用 Boost.Python 或 pybind11 等辅助库

  • 包含库头文件而不是直接包含 Python 头文件。
  • 极大地简化了对象的创建/销毁。
  • 提供包装器函数,而不是直接调用 Python API。
  • 自动进行内存管理的引用计数。
  • 异常转换和处理。

3. 从 C++ 创建和操作 Python 对象

  • 使用 PyList_New() 来创建列表、字典等。
  • 使用 PyTuple_SetItem() 来创建和填充元组。
  • 通过 PyType_Ready() 来包装自定义类。
  • 通过包装器层来创建对象,而不是直接通过 Python API。

4. 从 C++ 调用 Python 对象方法

  • 使用 PyInstanceMethod_New() 来获取绑定到实例的可调用对象。
  • PyEval_CallObject() 执行方法调用。
  • Boost/pybind11 包装器再次简化了操作。

5. 正确管理引用和内存

  • 通过 Py_XINCREF/DECREF 手动递增/递减每个对象的引用计数。
  • 遗漏可能导致因指针悬空而崩溃。
  • 辅助库提供自动引用计数。

因此,总而言之,通过 Python C API 集成 C++/Python 是可行的,但更底层且容易出错。pybind11 或 Boost 等辅助库。大大简化了从 C++ 代码包装和与 Python 对象交互的过程。它们处理繁琐的底层内存管理,同时暴露简单、原生的接口。

代码实现

1. 直接访问 Python C API

输出

[]

说明

  • “PyObject” 是在 C/C++ 中表示所有 Python 对象的基结构。
  • “PyList_New()” 是用于创建具有初始分配大小为 3 的新空 Python 列表的 C API 函数。
  • PyObject_Str() 将返回给定对象的 Python 字符串表示。
  • 我们使用 “PyUnicode_AsUTF8()” 将该 “PyObject” 字符串转换回 C 字符串。
  • 最后,我们使用 C “puts()” 函数打印 UTF-8 编码的字符串。

2. 使用 pybind11 辅助库

输出

[]

3. 从 C 创建和操作 Python 对象

输出

{'key': 10, 'other': 'value'}

4. 从 C++ 调用 Python 对象方法

输出

10

5. 正确管理内存

输出

 Ref count: 1

Python C API

在 C/C++ 中封装 Python 对象的基础是 Python/C API。Python 解释器公开的标准函数和接口集允许其他代码与 Python 对象进行交互。Python 本身就是建立在它之上的。

Python/C API 提供的一些关键功能包括:

  • 从 C/C++ 创建新的 Python 对象
  • 从 C/C++ 调用 Python 对象方法
  • 获取和设置 Python 对象上的属性
  • 正确处理引用计数和内存管理
  • 捕获和处理 Python 异常
  • 加载 Python 模块并调用其函数

因此,通过利用几十种可用的 API 调用,您可以从纯 C/C++ 代码中完全控制和操作 Python 对象和环境。

基本对象包装

从 C++ 包装基本 Python 对象(如整数)的步骤是:

  1. 包含 Python 头文件
  2. 调用 API 函数来创建一个新的 Python 整数对象
  3. 使用 API 函数调用对象上支持的方法
  4. 通过 API 访问属性
  5. 处理对象的引用计数

例如

1. 包含头文件

2. 创建整数

3. 操作对象

4. 属性访问

5. 引用处理

类包装

要包装自定义 Python 类,您需要:

  1. 使用 C 结构和函数重新创建该类。
  2. 将 Python 方法调用公开给链接的 C 函数。
  3. 向解释器注册包装器类。

这需要更多的努力,但提供了从 C++ 完全控制 Python 类的能力。

通过辅助库简化

手动处理 Python C API 可能很麻烦。因此,像 Boost.Pythonpybind11 这样的库通过高级包装器和自动引用计数极大地简化了这一过程。

例如,使用 pybind11

这些库使得在 C++ 中包装 Python 对象变得简洁且具有原生感。

关于生命周期管理、异常处理和高级包装技术还有更多细节。但这个概述涵盖了通过 Python C API(直接或通过辅助库)在 C/C++ 代码中公开 Python 对象的主要步骤。

结论

当您将 Python 的功能与 C/C++ 的性能相结合时,您就可以为需要生产力和速度的应用程序开辟新的可能性。通过使 C/C++ 可以访问 Python 对象,代码开发人员可以利用每种语言最适合其优势的地方。

Python/C API 是此过程的核心,它提供了一系列函数和接口,可用于从 C/C++ 代码与 Python 解释器和对象模型进行交互。使用此底层 API,C/C++ 程序可以轻松地创建 Python 对象,调用其方法,访问属性,并高效地管理内存。然而,手动使用 C API 包装 Python 涉及大量样板代码和引用计数处理。

Boost.Python 和 pybind11 等库提供了 Python API 的高级包装器,以简化此集成过程。只需几行 C++ 代码,这些库就可以自动生成绑定,以供 C++ 使用 Python 类、模块和对象。辅助库在级别上负责引用计数、异常处理和其他复杂细节。

这会在将 Python 集成到 C++ 时提供一种体验。当然,存在挑战,例如处理跨语言的对象管理以及频繁在 Python 和编译的 C/C++ 代码之间切换的潜在性能影响。与任何涉及语言的解决方案一样,重要的是要设计它以充分利用每种语言的优势。Python 对象可能不应该被包装到每个细微的例程中,但只专注于核心性能关键部分可以产生可观的收益。

将最新的 C++ 标准与 Python 成熟的对象模型相结合,可以在适当利用时成为强大的组合。C++ 开发人员可以利用 Python 丰富的 数据科学库和框架。Python 开发人员可以注入高性能内核,而不会牺牲编码生产力。无论用例如何,在 C/C++ 中包装 Python 对象都为在现代软件开发中统一简单性和速度这一挑战性任务提供了一个可行的解决方案。