关于python:Python-绑定从-Python-调用-C-或-C

6次阅读

共计 18292 个字符,预计需要花费 46 分钟才能阅读完成。

摘要:您是领有想要从 Python 中应用的 C 或 C++ 库的 Python 开发人员吗?如果是这样,那么 Python 绑定容许您调用函数并将数据从 Python 传递到 C 或 C ++,让您利用这两种语言的劣势。

本文分享自华为云社区《Python 绑定: 从 Python 调用 C 或 C++ |【成长吧!Python!】》,原文作者:Yuchuan。

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

在本教程中,您将理解:

  • 为什么要从 Python 调用 C 或 C++
  • 如何在 C 和 Python 之间传递数据
  • 哪些工具和办法能够帮忙您创立 Python 绑定

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

Python Bindings 概述

在深入研究如何从 Python 调用 C 之前,最好花一些工夫理解为什么. 有几种状况下,创立 Python 绑定来调用 C 库是一个好主见:

  1. 您曾经领有一个用 C++ 编写的大型、通过测试的稳固库,您想在 Python 中利用它。这可能是一个通信库或一个与特定硬件对话的库。它做什么并不重要。
  2. 您心愿 通过将要害局部转换为 C 来减速 Python 代码的特定局部。C 不仅具备更快的执行速度,而且还容许您解脱 GIL 的限度,前提是您小心。
  3. 您想应用 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,您须要执行以下步骤:

  1. 加载 您的库。
  2. 包装 一些输出参数。
  3. 通知 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 <module>
    answer = c_lib.cmult(x, y)
ctypes.ArgumentError: argument 2: <class 'TypeError'>: 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 文件,并将其链接到一个.<system-description>.so 或.<system-description>.dll 文件。
  • ‘#include “cmult.h”‘是自定义 C 源代码,它将在编译之前蕴含在生成的源代码中。在这里,您只需蕴含.h 要为其生成绑定的文件,但这可用于一些乏味的自定义。
  • libraries=[“cmult”]通知链接器您事后存在的 C 库的名称。这是一个列表,因而您能够依据须要指定多个库。
  • library_dirs=[this_dir.as_posix(),] 是一个目录列表,通知链接器在何处查找上述库列表。
  • extra_link_args=[‘-Wl,-rpath,.’]是一组生成共享对象的选项,它将在以后门路 (.) 中查找它须要加载的其余库。

构建 Python 绑定

调用.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 <pybind11/pybind11.h>
#include <cppmult.hpp>

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 绑定

用于构建 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 <cppmult.hpp> 解析包装器代码中的行。

第 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)

这里有两个局部:

  1. 线 3 和 4 告 诉 Cython 您应用的是 cppmult()从 cppmult.hpp。
  2. 第 6 行和第 7 行 创立了一个包装函数 pymult(),以调用 cppmult()。

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

第一局部 withcdef extern… 通知 Cython 上面的函数申明也能够在 cppmult.hpp 文件中找到。这对于确保依据与 C++ 代码雷同的申明构建 Python 绑定十分有用。第二局部看起来像一个一般的 Python 函数——因为它是!本节创立一个能够拜访 C++ 函数的 Python 函数 cppmult。

当初您曾经定义了 Python 绑定,是时候构建它们了!

构建 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是一个乏味的工具,它的设计指标与您目前所见略有不同。用包作者的话来说:

“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 运行命令行工作的工具。

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0