Python 绑定:从 Python 调用 C 或 C++ |【生长吧!Python!】 【生长吧!Python】有奖征文火热进行中:https://bbs.huaweicloud.com/blogs/278897

网友投稿 568 2022-05-29

Table of Contents

Python Bindings Overview

Marshalling Data Types

Understanding Mutable and Immutable Values

Managing Memory

Setting Up Your Environment

Using the invoke Tool

C or C++ Source

ctypes

How It’s Installed

Calling the Function

Strengths and Weaknesses

CFFI

How It’s Installed

Calling the Function

Strengths and Weaknesses

PyBind11

How It’s Installed

Calling the Function

Strengths and Weaknesses

Cython

How It’s Installed

Calling the Function

Strengths and Weaknesses

Other Solutions

PyBindGen

Boost.Python

SIP

Cppyy

Shiboken

SWIG

Conclusion

您是拥有想要从 Python 中使用的C或 C++ 库的 Python 开发人员吗?如果是这样,那么Python 绑定允许您调用函数并将数据从 Python 传递到C或C++,让您利用这两种语言的优势。在本教程中,您将看到一些可用于创建 Python 绑定的工具的概述。

在本教程中,您将了解:

为什么要从 Python调用 C 或 C++

如何在 C 和 Python 之间传递数据

哪些工具和方法可以帮助您创建 Python 绑定

本教程面向中级 Python 开发人员。它假定读者具备 Python 的基本知识,并对 C 或 C++ 中的函数和数据类型有所了解。您可以通过单击下面的链接获取本教程中将看到的所有示例代码:

Python Bindings概述

Python 绑定:从 Python 调用 C 或 C++ |【生长吧!Python!】 【生长吧!Python】有奖征文火热进行中:https://bbs.huaweicloud.com/blogs/278897

在深入研究如何从 Python 调用 C之前,最好花一些时间了解为什么. 有几种情况下,创建 Python 绑定来调用 C 库是一个好主意:

您已经拥有一个用 C++ 编写的大型、经过测试的稳定库,您想在 Python 中利用它。这可能是一个通信库或一个与特定硬件对话的库。它做什么并不重要。

您希望通过将关键部分转换为 C来加速 Python 代码的特定部分。 C 不仅具有更快的执行速度,而且还允许您摆脱GIL的限制,前提是您小心。

您想使用 Python 测试工具对其系统进行大规模测试。

以上所有都是学习创建 Python 绑定以与 C 库交互的重要原因。

注意:在本教程中,您将创建到 C和C++ 的Python 绑定。大多数通用概念适用于两种语言,因此除非两种语言之间存在特定差异,否则将使用 C。通常,每个工具都支持 C或C++,但不能同时支持两者。

让我们开始吧!

编组数据类型

等待!在开始编写 Python 绑定之前,先看看 Python 和 C 如何存储数据以及这会导致哪些类型的问题。首先,让我们定义编组。这个概念由维基百科定义如下:

将对象的内存表示转换为适合存储或传输的数据格式的过程。(来源)

出于您的目的,编组是 Python 绑定在准备数据以将其从 Python 移动到 C 或反之亦然时所做的工作。Python 绑定需要进行编组,因为 Python 和 C 以不同的方式存储数据。C 在内存中以最紧凑的形式存储数据。如果您使用uint8_t,那么它总共将只使用 8 位内存。

另一方面,在 Python 中,一切都是对象。这意味着每个整数在内存中使用几个字节。多少取决于您运行的 Python 版本、操作系统和其他因素。这意味着您的 Python 绑定将需要为每个跨边界传递的整数将C 整数转换为Python 整数。

其他数据类型在这两种语言之间具有相似的关系。让我们依次来看看:

整数存储计数数字。Python 以任意精度存储整数,这意味着您可以存储非常非常大的数字。C 指定整数的确切大小。在语言之间移动时需要注意数据大小,以防止 Python 整数值溢出 C 整数变量。

浮点数是带有小数位的数字。Python 可以存储比 C 大得多(和小得多)的浮点数。这意味着您还必须注意这些值以确保它们保持在范围内。

复数是带有虚部的数字。虽然 Python 具有内置复数,而 C 具有复数,但没有用于在它们之间编组的内置方法。要封送复数,您需要在 C 代码中构建struct或class来管理它们。

字符串是字符序列。作为这样一种常见的数据类型,当您创建 Python 绑定时,字符串将被证明是相当棘手的。与其他数据类型一样,Python 和 C 以完全不同的格式存储字符串。(与其他数据类型不同,这也是 C 和 C++ 不同的领域,这增加了乐趣!)您将研究的每个解决方案都有略微不同的处理字符串的方法。

布尔变量只能有两个值。由于它们在 C 中得到支持,因此将它们编组将被证明是相当简单的。

除了数据类型转换之外,在构建 Python 绑定时还需要考虑其他问题。让我们继续探索它们。

了解可变和不可变值

除了所有这些数据类型之外,您还必须了解 Python 对象如何可变或不可变。当谈到传值或传引用时,C 有一个类似的函数参数概念。在 C 中,所有参数都是按值传递的。如果要允许函数更改调用方中的变量,则需要传递指向该变量的指针。

您可能想知道是否可以通过使用指针将不可变对象简单地传递给 C 来绕过不可变限制。除非你走到丑陋和不可移植的极端,否则Python 不会给你一个指向 object 的指针,所以这行不通。如果您想用 C 修改 Python 对象,那么您需要采取额外的步骤来实现这一点。这些步骤将取决于您使用的工具,如下所示。

因此,您可以将不变性添加到您创建 Python 绑定时要考虑的项目清单中。在创建此清单的宏伟之旅中,您的最后一站是如何处理 Python 和 C 处理内存管理的不同方式。

管理内存

C 和 Python管理内存的方式不同。在 C 中,开发人员必须管理所有内存分配并确保它们被释放一次且仅一次。Python 使用垃圾收集器为您处理这个问题。

虽然这些方法中的每一种都有其优点,但它确实为创建 Python 绑定添加了额外的麻烦。您需要知道每个对象的内存分配在哪里,并确保它只在语言障碍的同一侧被释放。

例如,当您设置x = 3. 用于此的内存在 Python 端分配,需要进行垃圾收集。幸运的是,使用 Python 对象,很难做任何其他事情。看看 C 中的逆向,直接分配一块内存:

int* iPtr = (int*)malloc(sizeof(int));

执行此操作时,您需要确保在 C 中释放此指针。这可能意味着手动将代码添加到 Python 绑定中以执行此操作。

这完善了您的一般主题清单。让我们开始设置您的系统,以便您可以编写一些代码!

设置您的环境

在本教程中,您将使用来自 Real Python GitHub 存储库的预先存在的 C 和 C++ 库来展示每个工具的测试。目的是您将能够将这些想法用于任何 C 库。要遵循此处的所有示例,您需要具备以下条件:

安装的C++ 库和命令行调用路径的知识

Python开发工具:

对于 Linux,这是python3-dev或python3-devel包,具体取决于您的发行版。

对于 Windows,有多个选项。

Python 3.6或更高版本

一个虚拟环境(建议,但不要求)

该invoke工具

最后一个对你来说可能是新的,所以让我们仔细看看它。

使用invoke工具

invoke是您将在本教程中用于构建和测试 Python 绑定的工具。它具有类似的目的,make但使用 Python 而不是 Makefiles。您需要invoke使用pip以下命令在虚拟环境中安装:

$ python3 -m pip install invoke

要运行它,请键入invoke后跟要执行的任务:

$ invoke build-cmult ================================================== = Building C Library * Complete

要查看哪些任务可用,请使用以下--list选项:

$ invoke --list Available tasks: all Build and run all tests build-cffi Build the CFFI Python bindings build-cmult Build the shared library for the sample C code build-cppmult Build the shared library for the sample C++ code build-cython Build the cython extension module build-pybind11 Build the pybind11 wrapper library clean Remove any built objects test-cffi Run the script to test CFFI test-ctypes Run the script to test ctypes test-cython Run the script to test Cython test-pybind11 Run the script to test PyBind11

请注意,当您查看定义任务的tasks.py文件时invoke,您会看到列出的第二个任务的名称是build_cffi. 但是,来自的输出将其--list显示为build-cffi. 减号 ( -) 不能用作 Python 名称的一部分,因此该文件使用下划线 ( _) 代替。

对于您将检查的每个工具,都会定义一个build-和一个test-任务。例如,要运行 的代码CFFI,您可以键入invoke build-cffi test-cffi。一个例外是ctypes,因为 没有构建阶段ctypes。此外,为了方便,还添加了两个特殊任务:

invoke all 运行所有工具的构建和测试任务。

invoke clean 删除任何生成的文件。

既然您已经对如何运行代码有所了解,那么在查看工具概述之前,让我们先看一下您将要包装的 C 代码。

C 或 C++ 源代码

在下面的每个示例部分中,您将为C 或 C++ 中的相同函数创建 Python 绑定。这些部分旨在让您体验每种方法的外观,而不是有关该工具的深入教程,因此您将封装的函数很小。您将为其创建 Python 绑定的函数将 anint和 afloat作为输入参数并返回一个float是两个数字的乘积:

// cmult.c float cmult(int int_param, float float_param) { float return_value = int_param * float_param; printf(" In cmult : int: %d float %.1f returning %.1f\n", int_param, float_param, return_value); return return_value; }

C 和 C++ 函数几乎相同,它们之间的名称和字符串略有不同。您可以通过单击以下链接获取所有代码的副本:

现在您已经克隆了 repo 并安装了工具,您可以构建和测试这些工具。因此,让我们深入了解下面的每个部分!

ctypes

您将从 开始ctypes,它是标准库中用于创建 Python 绑定的工具。它提供了一个低级工具集,用于在 Python 和 C 之间加载共享库和编组数据。

它是如何安装的

的一大优点ctypes是它是 Python 标准库的一部分。它是在 Python 2.5 版中添加的,因此您很可能已经拥有它。您可以import像使用sys或time模块一样。

调用函数

加载 C 库和调用函数的所有代码都将在 Python 程序中。这很棒,因为您的过程中没有额外的步骤。您只需运行您的程序,一切都会得到处理。要在 中创建 Python 绑定ctypes,您需要执行以下步骤:

加载您的库。

包装一些输入参数。

告诉 ctypes你函数的返回类型。

您将依次查看其中的每一个。

ctypes为您提供了多种加载共享库的方法,其中一些是特定于平台的。对于您的示例,您将ctypes.CDLL通过传入所需共享库的完整路径来直接创建对象:

# ctypes_test.py import ctypes import pathlib if __name__ == "__main__": # Load the shared library into ctypes libname = pathlib.Path().absolute() / "libcmult.so" c_lib = ctypes.CDLL(libname)

这适用于共享库与 Python 脚本位于同一目录中的情况,但在尝试加载来自 Python 绑定以外的包的库时要小心。在ctypes特定于平台和特定情况的文档中,有许多关于加载库和查找路径的详细信息。

注意:在库加载过程中可能会出现许多特定于平台的问题。最好在示例工作后进行增量更改。

现在您已将库加载到 Python 中,您可以尝试调用它!

请记住,您的 C 函数的函数原型如下:

// cmult.h float cmult(int int_param, float float_param);

您需要传入一个整数和一个浮点数,并且可以期望得到一个浮点数返回。整数和浮点数在 Python 和 C 中都有本机支持,因此您希望这种情况适用于合理的值。

将库加载到 Python 绑定中后,该函数将成为 的属性c_lib,即CDLL您之前创建的对象。您可以尝试这样称呼它:

x, y = 6, 2.3 answer = c_lib.cmult(x, y)

哎呀!这不起作用。此行在示例 repo 中被注释掉,因为它失败了。如果您尝试使用该调用运行,那么 Python 会报错:

$ invoke test-ctypes Traceback (most recent call last): File "ctypes_test.py", line 16, in answer = c_lib.cmult(x, y) ctypes.ArgumentError: argument 2: : Don't know how to convert parameter 2

看起来您需要说明ctypes任何不是整数的参数。ctypes除非您明确告诉它,否则您对该函数一无所知。任何未以其他方式标记的参数都假定为整数。ctypes不知道如何将2.3存储的值转换为y整数,所以它失败了。

要解决此问题,您需要c_float从号码中创建一个。您可以在调用函数的行中执行此操作:

# ctypes_test.py answer = c_lib.cmult(x, ctypes.c_float(y)) print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")

现在,当您运行此代码时,它会返回您传入的两个数字的乘积:

$ invoke test-ctypes In cmult : int: 6 float 2.3 returning 13.8 In Python: int: 6 float 2.3 return val 48.0

等一下……6乘以2.3不是48.0!

事实证明,就像输入参数一样,ctypes 假设您的函数返回一个int. 实际上,您的函数返回 a float,它被错误地编组。就像输入参数一样,您需要告诉ctypes使用不同的类型。这里的语法略有不同:

# ctypes_test.py c_lib.cmult.restype = ctypes.c_float answer = c_lib.cmult(x, ctypes.c_float(y)) print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")

这应该够了吧。让我们运行整个test-ctypes目标,看看你有什么。请记住,输出的第一部分是在restype将函数固定为浮点数之前:

$ invoke test-ctypes ================================================== = Building C Library * Complete ================================================== = Testing ctypes Module In cmult : int: 6 float 2.3 returning 13.8 In Python: int: 6 float 2.3 return val 48.0 In cmult : int: 6 float 2.3 returning 13.8 In Python: int: 6 float 2.3 return val 13.8

这样更好!虽然第一个未更正的版本返回错误的值,但您的固定版本与 C 函数一致。C 和 Python 都得到相同的结果!现在它可以工作了,看看为什么您可能想或不想使用ctypes.

长处和短处

ctypes与您将在此处检查的其他工具相比,最大的优势在于它内置于标准库中。它还不需要额外的步骤,因为所有工作都是作为 Python 程序的一部分完成的。

此外,所使用的概念是低级的,这使得像您刚刚做的那样的练习易于管理。然而,由于缺乏自动化,更复杂的任务变得繁琐。在下一部分中,您将看到一个工具,该工具为流程添加了一些自动化。

CFFI

CFFI是Python的C 外来函数接口。生成 Python 绑定需要更自动化的方法。CFFI有多种方式可以构建和使用 Python 绑定。有两种不同的选项可供选择,为您提供四种可能的模式:

ABI vs API: API 模式使用 C 编译器生成完整的 Python 模块,而 ABI 模式加载共享库并直接与其交互。在不运行编译器的情况下,获取正确的结构和参数很容易出错。该文档强烈建议使用 API 模式。

内联 vs 外联:这两种模式的区别在于速度和便利性之间的权衡:

每次运行脚本时,内联模式都会编译 Python 绑定。这很方便,因为您不需要额外的构建步骤。但是,它确实会减慢您的程序速度。

Out-of-line 模式需要一个额外的步骤来一次性生成 Python 绑定,然后在每次程序运行时使用它们。这要快得多,但这对您的应用程序可能无关紧要。

对于此示例,您将使用 API 外联模式,它生成最快的代码,并且通常看起来类似于您将在本教程后面创建的其他 Python 绑定。

它是如何安装的

由于CFFI不是标准库的一部分,您需要在您的机器上安装它。建议您为此创建一个虚拟环境。幸运的是,CFFI安装有pip:

$ python3 -m pip install cffi

这会将软件包安装到您的虚拟环境中。如果您已经从 安装requirements.txt,那么应该注意这一点。您可以requirements.txt通过访问以下链接中的 repo 来查看:

获取示例代码: 单击此处获取您将用于在本教程中了解 Python 绑定的示例代码。

现在你已经CFFI安装好了,是时候试一试了!

调用函数

与 不同的是ctypes,CFFI您正在创建一个完整的 Python 模块。您将能够import像标准库中的任何其他模块一样使用该模块。您需要做一些额外的工作来构建 Python 模块。要使用CFFIPython 绑定,您需要执行以下步骤:

编写一些描述绑定的 Python 代码。

运行该代码以生成可加载模块。

修改调用代码以导入和使用新创建的模块。

这可能看起来需要做很多工作,但您将完成这些步骤中的每一个,并了解它是如何工作的。

CFFI提供读取C 头文件的方法,以在生成 Python 绑定时完成大部分工作。在 的文档中CFFI,执行此操作的代码放置在单独的 Python 文件中。对于此示例,您将直接将该代码放入构建工具中invoke,该工具使用 Python 文件作为输入。要使用CFFI,您首先要创建一个cffi.FFI对象,该对象提供了您需要的三种方法:

# tasks.py import cffi ... """ Build the CFFI Python bindings """ print_banner("Building CFFI Module") ffi = cffi.FFI()

拥有 FFI 后,您将使用.cdef()来自动处理头文件的内容。这会为您创建包装函数以从 Python 封送数据:

# tasks.py this_dir = pathlib.Path().absolute() h_file_name = this_dir / "cmult.h" with open(h_file_name) as h_file: ffi.cdef(h_file.read())

读取和处理头文件是第一步。之后,您需要使用.set_source()来描述CFFI将生成的源文件:

# tasks.py ffi.set_source( "cffi_example", # Since you're calling a fully-built library directly, no custom source # is necessary. You need to include the .h files, though, because behind # the scenes cffi generates a .c file that contains a Python-friendly # wrapper around each of the functions. '#include "cmult.h"', # The important thing is to include the pre-built lib in the list of # libraries you're linking against: libraries=["cmult"], library_dirs=[this_dir.as_posix()], extra_link_args=["-Wl,-rpath,."], )

以下是您传入的参数的细分:

"cffi_example"是将在您的文件系统上创建的源文件的基本名称。CFFI将生成一个.c文件,将其编译为一个.o文件,并将其链接到一个..so或..dll文件。

'#include "cmult.h"'是自定义 C 源代码,它将在编译之前包含在生成的源代码中。在这里,您只需包含.h要为其生成绑定的文件,但这可用于一些有趣的自定义。

libraries=["cmult"]告诉链接器您预先存在的 C 库的名称。这是一个列表,因此您可以根据需要指定多个库。

library_dirs=[this_dir.as_posix(),] 是一个目录列表,告诉链接器在何处查找上述库列表。

extra_link_args=['-Wl,-rpath,.']是一组生成共享对象的选项,它将在当前路径 ( .) 中查找它需要加载的其他库。

调用.set_source()不会构建 Python 绑定。它只设置元数据来描述将生成的内容。要构建 Python 绑定,您需要调用.compile():

# tasks.py ffi.compile()

这通过生成.c文件、.o文件和共享库来完成。在invoke你刚走通过任务可以在上运行命令行构建Python绑定:

$ invoke build-cffi ================================================== = Building C Library * Complete ================================================== = Building CFFI Module * Complete

你有你的CFFIPython 绑定,所以是时候运行这段代码了!

在您为配置和运行CFFI编译器所做的所有工作之后,使用生成的 Python 绑定看起来就像使用任何其他 Python 模块一样:

# cffi_test.py import cffi_example if __name__ == "__main__": # Sample data for your call x, y = 6, 2.3 answer = cffi_example.lib.cmult(x, y) print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")

你导入新模块,然后就可以cmult()直接调用了。要对其进行测试,请使用以下test-cffi任务:

$ invoke test-cffi ================================================== = Testing CFFI Module In cmult : int: 6 float 2.3 returning 13.8 In Python: int: 6 float 2.3 return val 13.8

这将运行您的cffi_test.py程序,该程序会测试您使用CFFI. 关于编写和使用CFFIPython 绑定的部分到此结束。

长处和短处

ctypes与CFFI您刚刚看到的示例相比,这似乎需要更少的工作。虽然这对于这个用例来说是正确的,CFFI但与ctypes由于大部分功能包装的自动化相比,它可以更好地扩展到更大的项目。

CFFI也产生了完全不同的用户体验。ctypes允许您将预先存在的 C 库直接加载到您的 Python 程序中。CFFI,另一方面,创建一个可以像其他 Python 模块一样加载的新 Python 模块。

更重要的是,使用上面使用的外部 API方法,创建 Python 绑定的时间损失在您构建它时完成一次,并且不会在每次运行代码时发生。对于小程序来说,这可能不是什么大问题,但也可以通过CFFI这种方式更好地扩展到更大的项目。

就像ctypes, usingCFFI只允许您直接与 C 库交互。C++ 库需要大量的工作才能使用。在下一节中,您将看到一个专注于 C++ 的 Python 绑定工具。

PyBind11

PyBind11使用完全不同的方法来创建 Python 绑定。除了将重点从 C 转移到 C++ 之外,它还使用 C++ 来指定和构建模块,使其能够利用 C++ 中的元编程工具。像 一样CFFI,生成的 Python 绑定PyBind11是一个完整的 Python 模块,可以直接导入和使用。

PyBind11以Boost::Python库为蓝本并具有类似的界面。但是,它将其使用限制为 C++11 和更新版本,与支持所有内容的 Boost 相比,这使其能够简化和加快处理速度。

它是如何安装的

文档的“第一步”部分将PyBind11引导您了解如何下载和构建PyBind11. 虽然这似乎不是严格要求,但完成这些步骤将确保您设置了正确的 C++ 和 Python 工具。

注:大部分示例PyBind11使用cmake,是构建 C 和 C++ 项目的好工具。但是,对于此演示,您将继续使用该invoke工具,该工具遵循文档的手动构建部分中的说明。

您需要将此工具安装到您的虚拟环境中:

$ python3 -m pip install pybind11

PyBind11是一个全头库,类似于 Boost 的大部分内容。这允许pip将库的实际 C++ 源代码直接安装到您的虚拟环境中。

调用函数

在您深入研究之前,请注意您使用的是不同的 C++ 源文件, cppmult.cpp,而不是您用于前面示例的 C 文件。两种语言的功能基本相同。

与 类似CFFI,您需要创建一些代码来告诉该工具如何构建您的 Python 绑定。与 不同CFFI,此代码将使用 C++ 而不是 Python。幸运的是,只需要很少的代码:

// pybind11_wrapper.cpp #include #include PYBIND11_MODULE(pybind11_example, m) { m.doc() = "pybind11 example plugin"; // Optional module docstring m.def("cpp_function", &cppmult, "A function that multiplies two numbers"); }

让我们一次一个地看,因为PyBind11将大量信息打包成几行。

前两行包括pybind11.hC++ 库的文件和头文件cppmult.hpp. 之后,你就有了PYBIND11_MODULE宏。这将扩展为PyBind11源代码中详细描述的 C++ 代码块:

此宏创建入口点,当 Python 解释器导入扩展模块时将调用该入口点。模块名称作为第一个参数给出,不应用引号引起来。第二个宏参数定义了一个py::module可用于初始化模块的类型变量。(来源)

这对您来说意味着,在本例中,您正在创建一个名为的模块pybind11_example,其余代码将m用作py::module对象的名称。在下一行,在您定义的 C++ 函数中,您为模块创建一个文档字符串。虽然这是可选的,但让您的模块更加Pythonic是一个不错的选择。

最后,你有m.def()电话。这将定义一个由您的新 Python 绑定导出的函数,这意味着它将在 Python 中可见。在此示例中,您将传递三个参数:

cpp_function是您将在 Python 中使用的函数的导出名称。如本例所示,它不需要匹配 C++ 函数的名称。

&cppmult 获取要导出的函数的地址。

"A function..." 是函数的可选文档字符串。

现在您已经有了 Python 绑定的代码,接下来看看如何将其构建到 Python 模块中。

用于构建 Python 绑定的工具PyBind11是 C++ 编译器本身。您可能需要修改编译器和操作系统的默认值。

首先,您必须构建要为其创建绑定的 C++ 库。对于这么小的示例,您可以将cppmult库直接构建到 Python 绑定库中。但是,对于大多数实际示例,您将有一个要包装的预先存在的库,因此您将cppmult单独构建该库。构建是对编译器的标准调用以构建共享库:

# tasks.py invoke.run( "g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC cppmult.cpp " "-o libcppmult.so " )

运行这个invoke build-cppmult产生libcppmult.so:

$ invoke build-cppmult ================================================== = Building C++ Library * Complete

另一方面,Python 绑定的构建需要一些特殊的细节:

1# tasks.py 2invoke.run( 3 "g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC " 4 "`python3 -m pybind11 --includes` " 5 "-I /usr/include/python3.7 -I . " 6 "{0} " 7 "-o {1}`python3.7-config --extension-suffix` " 8 "-L. -lcppmult -Wl,-rpath,.".format(cpp_name, extension_name) 9)

让我们逐行浏览一下。第 3 行包含相当标准的 C++ 编译器标志,指示几个细节,包括您希望捕获所有警告并将其视为错误、您需要共享库以及您使用的是 C++11。

第 4 行是魔法的第一步。它调用pybind11模块使其include为PyBind11. 您可以直接在控制台上运行此命令以查看它的作用:

$ python3 -m pybind11 --includes -I/home/jima/.virtualenvs/realpython/include/python3.7m -I/home/jima/.virtualenvs/realpython/include/site/python3.7

您的输出应该相似但显示不同的路径。

在编译调用的第 5 行,您可以看到您还添加了 Python dev 的路径includes。虽然建议您不要链接 Python 库本身,但源代码需要一些代码Python.h才能发挥其魔力。幸运的是,它使用的代码在 Python 版本中相当稳定。

第 5 行还用于-I .将当前目录添加到include路径列表中。这允许#include 解析包装器代码中的行。

第 6 行指定源文件的名称,即pybind11_wrapper.cpp. 然后,在第 7 行,您会看到更多的构建魔法正在发生。此行指定输出文件的名称。Python 在模块命名上有一些特别的想法,包括 Python 版本、机器架构和其他细节。Python 还提供了一个工具来帮助解决这个问题python3.7-config:

$ python3.7-config --extension-suffix .cpython-37m-x86_64-linux-gnu.so

如果您使用的是不同版本的 Python,则可能需要修改该命令。如果您使用不同版本的 Python 或在不同的操作系统上,您的结果可能会发生变化。

构建命令的最后一行,第 8 行,将链接器指向libcppmult您之前构建的库。该rpath部分告诉链接器向共享库添加信息以帮助操作系统libcppmult在运行时查找。最后,您会注意到此字符串的格式为cpp_name和extension_name。Cython在下一节中构建 Python 绑定模块时,您将再次使用此函数。

运行此命令以构建绑定:

$ invoke build-pybind11 ================================================== = Building C++ Library * Complete ================================================== = Building PyBind11 Module * Complete

就是这样!您已经使用PyBind11. 是时候测试一下了!

与CFFI上面的示例类似,一旦您完成了创建 Python 绑定的繁重工作,调用您的函数看起来就像普通的 Python 代码:

# pybind11_test.py import pybind11_example if __name__ == "__main__": # Sample data for your call x, y = 6, 2.3 answer = pybind11_example.cpp_function(x, y) print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")

由于您pybind11_example在PYBIND11_MODULE宏中用作模块的名称,因此这就是您导入的名称。在m.def()您告诉PyBind11将cppmult函数导出为 的调用中cpp_function,这就是您用来从 Python 调用它的方法。

你也可以测试它invoke:

$ invoke test-pybind11 ================================================== = Testing PyBind11 Module In cppmul: int: 6 float 2.3 returning 13.8 In Python: int: 6 float 2.3 return val 13.8

这就是PyBind11看起来的样子。接下来,您将了解何时以及为何PyBind11是适合该工作的工具。

长处和短处

PyBind11专注于 C++ 而不是 C,这使得它不同于ctypes和CFFI。它有几个特性使其对 C++ 库非常有吸引力:

它支持类。

它处理多态子类化。

它允许您从 Python 和许多其他工具向对象添加动态属性,而使用您检查过的基于 C 的工具很难做到这一点。

话虽如此,您需要进行大量设置和配置才能PyBind11启动和运行。正确安装和构建可能有点挑剔,但一旦完成,它似乎相当可靠。此外,PyBind11要求您至少使用 C++11 或更高版本。对于大多数项目来说,这不太可能是一个很大的限制,但它可能是您的一个考虑因素。

最后,创建 Python 绑定需要编写的额外代码是用 C++ 编写的,而不是用 Python 编写的。这可能是也可能不是你的问题,但它是比你在这里看到的其他工具不同。在下一节中,您将继续讨论Cython,它采用完全不同的方法来解决这个问题。

Cython

该方法Cython需要创建Python绑定使用类Python语言来定义绑定,然后生成的C或C ++代码可被编译成模块。有几种方法可以使用Cython. 最常见的一种是使用setupfrom distutils。对于此示例,您将坚持使用该invoke工具,它允许您使用运行的确切命令。

它是如何安装的

Cython是一个 Python 模块,可以从PyPI安装到您的虚拟环境中:

$ python3 -m pip install cython

同样,如果您已将该requirements.txt文件安装到虚拟环境中,则该文件已经存在。您可以requirements.txt通过单击以下链接获取副本:

获取示例代码: 单击此处获取您将用于在本教程中了解 Python 绑定的示例代码。

这应该让你准备好与之合作Cython!

调用函数

要使用 构建 Python 绑定Cython,您将遵循与用于CFFI和 的步骤类似的步骤PyBind11。您将编写绑定、构建它们,然后运行 Python 代码来调用它们。Cython可以同时支持 C 和 C++。对于本示例,您将使用cppmult您在PyBind11上面的示例中使用的库。

声明模块的最常见形式Cython是使用.pyx文件:

1# cython_example.pyx 2""" Example cython interface definition """ 3 4cdef extern from "cppmult.hpp": 5 float cppmult(int int_param, float float_param) 6 7def pymult( int_param, float_param ): 8 return cppmult( int_param, float_param )

这里有两个部分:

线3和4告诉Cython您使用的是cppmult()从cppmult.hpp。

第 6 行和第 7 行创建了一个包装函数pymult(),以调用cppmult()。

这里使用的语言是 C、C++ 和 Python 的特殊组合。不过,对于 Python 开发人员来说,它看起来相当熟悉,因为其目标是使过程更容易。

第一部分 withcdef extern...告诉Cython下面的函数声明也可以在cppmult.hpp文件中找到。这对于确保根据与 C++ 代码相同的声明构建 Python 绑定非常有用。第二部分看起来像一个普通的 Python 函数——因为它是!本节创建一个可以访问 C++ 函数的 Python 函数cppmult。

现在您已经定义了 Python 绑定,是时候构建它们了!

的构建过程Cython与您使用的构建过程相似PyBind11。您首先Cython在.pyx文件上运行以生成.cpp文件。完成此操作后,您可以使用用于以下内容的相同函数对其进行编译PyBind11:

1# tasks.py 2def compile_python_module(cpp_name, extension_name): 3 invoke.run( 4 "g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC " 5 "`python3 -m pybind11 --includes` " 6 "-I /usr/include/python3.7 -I . " 7 "{0} " 8 "-o {1}`python3.7-config --extension-suffix` " 9 "-L. -lcppmult -Wl,-rpath,.".format(cpp_name, extension_name) 10 ) 11 12def build_cython(c): 13 """ Build the cython extension module """ 14 print_banner("Building Cython Module") 15 # Run cython on the pyx file to create a .cpp file 16 invoke.run("cython --cplus -3 cython_example.pyx -o cython_wrapper.cpp") 17 18 # Compile and link the cython wrapper library 19 compile_python_module("cython_wrapper.cpp", "cython_example") 20 print("* Complete")

您首先运行cython您的.pyx文件。您可以在此命令上使用几个选项:

--cplus 告诉编译器生成 C++ 文件而不是 C 文件。

-3切换Cython到生成 Python 3 语法而不是 Python 2。

-o cython_wrapper.cpp 指定要生成的文件的名称。

生成 C++ 文件后,您可以使用 C++ 编译器生成 Python 绑定,就像您为PyBind11. 请注意,include使用该pybind11工具生成额外路径的调用仍在该函数中。在这里不会有任何伤害,因为您的来源不需要这些。

在 中运行此任务invoke会产生以下输出:

$ invoke build-cython ================================================== = Building C++ Library * Complete ================================================== = Building Cython Module * Complete

可以看到它构建了cppmult库,然后构建了cython模块来包装它。现在你有了CythonPython 绑定。(试着说的是迅速...)它的时间来测试一下吧!

调用新 Python 绑定的 Python 代码与用于测试其他模块的代码非常相似:

1# cython_test.py 2import cython_example 3 4# Sample data for your call 5x, y = 6, 2.3 6 7answer = cython_example.pymult(x, y) 8print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")

第 2 行导入新的 Python 绑定模块,并pymult()在第 7 行调用。请记住,该.pyx文件提供了一个 Python 包装器cppmult()并将其重命名为pymult. 使用 invoke 运行您的测试会产生以下结果:

$ invoke test-cython ================================================== = Testing Cython Module In cppmul: int: 6 float 2.3 returning 13.8 In Python: int: 6 float 2.3 return val 13.8

你得到和以前一样的结果!

长处和短处

Cython是一个相对复杂的工具,可以在为 C 或 C++ 创建 Python 绑定时为您提供更深层次的控制。虽然您没有在此处深入介绍它,但它提供了一种 Python 式的方法来编写手动控制GIL 的代码,这可以显着加快某些类型的问题的处理速度。

然而,这种 Python 风格的语言并不完全是 Python,因此当您要快速确定 C 和 Python 的哪些部分适合何处时,会有一个轻微的学习曲线。

其他解决方案

在研究本教程时,我遇到了几种用于创建 Python 绑定的不同工具和选项。虽然我将此概述限制为一些更常见的选项,但我偶然发现了其他几种工具。下面的列表并不全面。如果上述工具之一不适合您的项目,这只是其他可能性的一个示例。

PyBindGen

PyBindGen为 C 或 C++ 生成 Python 绑定并用 Python 编写。它旨在生成可读的 C 或 C++ 代码,这应该可以简化调试问题。目前尚不清楚这是否最近已更新,因为文档将 Python 3.4 列为最新的测试版本。然而,在过去的几年里,每年都有发布。

Boost.Python

Boost.Python有一个类似于PyBind11您在上面看到的界面。这不是巧合,因为PyBind11它基于这个库!Boost.Python是用完整的 C++ 编写的,并且在大多数平台上支持大多数(如果不是全部)C++ 版本。相比之下,PyBind11仅限于现代 C++。

SIP

SIP是为PyQt项目开发的用于生成 Python 绑定的工具集。wxPython项目也使用它来生成它们的绑定。它有一个代码生成工具和一个额外的 Python 模块,为生成的代码提供支持功能。

Cppyy

“cppyy 背后的最初想法(追溯到 2001 年)是允许生活在 C++ 世界中的 Python 程序员访问那些 C++ 包,而不必直接接触 C++(或等待 C++ 开发人员过来并提供绑定) 。” (来源)

Shiboken

Shiboken是为与 Qt 项目关联的 PySide 项目开发的用于生成 Python 绑定的工具。虽然它被设计为该项目的工具,但文档表明它既不是 Qt 也不是 PySide 特定的,可用于其他项目。

SWIG

SWIG是与此处列出的任何其他工具不同的工具。它是一个通用工具,用于为许多其他语言(而不仅仅是 Python)创建到 C 和 C++ 程序的绑定。这种为不同语言生成绑定的能力在某些项目中非常有用。当然,就复杂性而言,它会带来成本。

结论

恭喜!您现在已经大致了解了用于创建Python 绑定的几个不同选项。您已经了解了编组数据以及创建绑定时需要考虑的问题。您已经了解了如何使用以下工具从 Python 调用 C 或 C++ 函数:

ctypes

CFFI

PyBind11

Cython

您现在知道,虽然ctypes允许您直接加载 DLL 或共享库,但其他三个工具需要额外的步骤,但仍会创建完整的 Python 模块。作为奖励,您还使用了invoke从 Python 运行命令行任务的工具。您可以通过单击下面的链接获取在本教程中看到的所有代码:

【生长吧!Python】有奖征文火热进行中:https://bbs.huaweicloud.com/blogs/278897

C++ Python

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Spring之5种Bean Scope
下一篇:文件共享之FTP服务
相关文章