Python 高级编程(第2版)--第7章 使用其他语言开发 Python 扩展

使用其他语言开发 Python 扩展

普通 Python 代码的限制:

  • 由于 GIL 的存在,线程可用性大大降低。
  • 它不编译。
  • 它不提供静态类型和相关的优化。

扩展,使用完全不同的语言进行编写,并通过 Python 扩展 API 公开其接口,这种解决方案有助于克服上述核心限制。

使用 C 或者 C++ 编写扩展

只要扩展提供使用 Python/C API 的适配接口,Python 解释器能够从动态/共享库加载它们。此 API 必须通过 C 头文件-Pythoh.h 引入到到扩展的源代码中,这个头文件通常随着 Python 源代码分发。

Python/C API 随着每个 Python 版本的发布而改变。在大多数情况下,只是对这些 API 增加新的功能,因此它通常会保持向下兼容。但是,在很多时候,由于应用程序二进制接口(Application Binary Interface, ABI)的更改,它们不是二进制兼容的。这意味着扩展必须为每个 Python 版本单独构建。

自从 Python 3.2, Python/C API 的一个子集已被定义为拥有稳定的 ABIs。然后可以使用这些有限的 API(具有稳定的ABI)构建扩展,这些扩展只要构建一次,就可以在任何高于或等于 3.2 的 Python 版本中正常工作,并且不需要重新编译。

Python/C API 是一个限于 CPython 实现的特性。

在使用 Python 的 C 扩展之前,需要把它编译成共享/动态库。distutils 和 setuptools 提供了辅助程序将编译好的扩展定义为模块,因此编译和分发可以使用 setup.py 脚本处理,就像它们是普通的 Python 包。

为什么你想用扩展

对于有些问题,扩展可能特别有用。

  • 在 Python 线程模型中绕过全局解释器锁(GlobalInterpreter Lock, GIL)。
  • 提高关键代码段的性能。
  • 集成第三方动态库。
  • 集成以不同语言编写的源代码。
  • 创建自定义数据类型。

提高关键代码段的性能

在大多数情况下,解决性能问题只是取决于选择正确的算法和数据结构,而不是编程语言天花板的限制因素。如果代码编写的有问题或者没有使用合适的算法,为了缩减一些 CPU 的周期而去依赖一个扩展,实际上,这不是一个好的解决方案。通常可以将性能提高到可接受的水平,而不需要通过将另一种语言引入到当前技术栈中来增加项目的复杂性。

集成现有的使用不同语言编写的代码

C 和 C++ 语言似乎是最重要的语言,它们提供了许多库和实现,你可以直接把它们集成到你的的应用程序代码中,而不需要将它们完全移植到 Python。CPython 已经用 C 语言编写,所以集成这样的代码的最自然的方式是通过自定义扩展。

集成第三方动态库

集成使用不同技术编写的代码不会随着 C/C++ 结束。许多库,特别是那些封闭源的第三方软件,以编译二进制文件的格式被分发。在 C 中,加载这样的共享/动态库并调用它们的函数是很容易的。这意味着你可以使用任何 C 库,只要使用 Python/C API 通过扩展包装它。

这不是唯一的解决方案,有一些工具,如 ctypes 或 CFFI,这些工具允许你使用纯 Python 与动态库交互,而不需要在 C 中编写扩展。很多时候,Python/C API 可能仍然是一个更好的选择,因为它提供了一个更好的集成层(用 C 编写),可以分离其余的应用程序。

创建自定义数据类型

Python 提供了多种多样的可选的内置数据类型。也可以在 Python 中创建许多自定义数据结构,完全基于某些内置类型构建它们,或者从头开始构建一个全新的类。

编写扩展

使用诸如 Cython 或 Pyrex 之类的工具,或者使用 ctypes 或 cffi 简单地与现有的动态库集成,通过这些方法开发自己的扩展是最简单的,也是值得推荐的方法。这些项目会极大的提高你的开发效率,并且降低代码的开发难度,使代码具有更好的可读性与可维护性。

纯 C 扩展

  • Python/C API 详情。

    扩展模块以一个包含 Python.h 头文件的单独的 C 预处理器指令开始。#include <Python.h> 它把整个 Python/C API 以及编写扩展需要引入的一切包含进来。在更现实的开发中,你的代码可能需要更多的预处理器指令,这样可以直接使用 C 标准库函数或者集成其他源文件。

  • 调用与绑定约定。

    Python/C API中的参数解析非常灵活,在官方文档 https://docs.python.org/3.5/c-api/arg.html 中有详细描述。Python 中已知的每个高级调用约定都可以用 C 编码,Python 中已知的每个高级调用约定都可以用这些APIC编码,包括以下几个。

    • 带参数默认值的函数。
    • 参数规定为仅关键字参数的函数。
    • 具有可变数量参数的函数。
  • 异常处理。

    C,不像 Python,或者甚至 C++ 没有抛出和捕获异常的语法。所有错误处理通常使用函数返回值和可选的全局状态来处理,用于保存可以解释上一个故障原因的详细信息。

    Python/C API 中的异常处理是围绕这个简单的原则构建的。在 C API 中发生并运行的最后一个错误在每个线程中都有一个全局指示符。它被设置为描述问题的原因。还有一种标准化的方式来通知函数的调用者,该状态在调用期间是否发生变化。

    • 如果函数应返回一个指针,则返回 NULL。
    • 如果函数返回一个 int 类型,则返回 -1。

    在 Python/C API 中,前面规则有一个唯一例外是 PyArg_*() 函数,返回 1 表示成功,0 表示失败。

    还有一个注意事项是,全局错误状态不会自我清除。一些错误可以在 C 函数中正常处理(与在 Python 中使用 try … except 子句相同),并且如果错误指示符不再有效,则需要清除错误指示符。

  • 释放 GIL。

    扩展是一种可以绕过 Python GIL 的方法。CPython 实现有一个著名的限制,声明每次只有一个线程可以执行 Python 代码。虽然多进程是绕过这个问题的建议方法,但是由于运行附加进程的资源开销,对于某些高度并行化的算法来说,多线程可能也不是一个好的解决方案。

    因为扩展主要用于大部分工作在纯C中执行而没有对 Python/C API 的任何调用的情况,所以可以(即使适当)在某些应用程序段中释放 GIL(甚至是明智的)。

  • 引用计数。

    Python 有自己的垃圾收集器,但它只是为了解决引用计数(referencecounting)算法中的循环引用的问题。引用计数是管理垃圾对象的重新分配的主要方法。

    Python 中的对象不是独占的,它们始终共享。对象的实际创建由 Python 的内存管理器管理。它是 CPython 解释器的组件,负责为存储在私有堆中的对象分配和释放内存。它可以持有对象的引用。

    由引用(PyObject * 指针)表示的 Python 中的每个对象都有一个相关的引用计数。当它变为 0 时,它意味着没有对象持有该对象的任何有效引用,就可以调用与其类型相关联的释放器。Python/C API 提供了两个宏来增加和减少引用计数:Py_INCREF() 和 Py_DECREF()。

    • 传递所有权(Passing of ownership):每当我们说函数将所有权传递给引用时,这意味着它已经增加了引用计数,并且当不再需要该引用对象时,调用者负责减少计数。
    • 借用引用(Borrowed references):引用的借用发生在函数接收到对某个Python对象的引用作为参数时。不应该在该函数中减少此引用的引用计数,除非它的范围明确增加。
    • 盗用引用(Stolen references):Python/C API 函数也有可能盗用引用,而不是在作为调用参数时借用它。

    另一个常见的问题是由 Python 对象模型的本质引起的,事实上,一些函数返回的是借用引用。当引用计数变为 0 时,释放函数会执行。对于用户定义的类,可以定义一个 __del__() 方法,该方法会在此时调用。这可以是任何 Python 代码,它可能会影响其他对象及其引用计数。

Cython

Cython 既是一个优化的静态编译器,也是一个 Python 的超集的编程语言的名称。作为编译器,它可以使用 Python/C API 执行源到源的编译,把本地 Python 代码及其 Cython 方言编译为 Python C 扩展。它允许你结合 Python 和 C 的威力,而不需要手动处理 Python/C API。

  • Cython 作为源码编译器。

    对于使用 Cython 创建的扩展,你将获得的主要优势是使用它提供的语言超集。几乎不需要对代码进行任何修改,就可以显著的提升性能,并且开发成本也非常低。

    Cython 提供了一个简单实用的 cythonize 函数,允许你轻松地将编译过程与 distutils 或 setuptools 集成。

    Cython 用作 Python 语言的源代码编译工具有另一个好处。源到源编译到扩展可以是源分发安装过程的完全可选部分。如果需要安装软件包的环境没有 Cython 或任何其他构建前提条件,则可以将其安装为普通的纯 Python 包。

    分发使用 Cython 构建的扩展的常见方法是打包 Python/Cython 源以及从这些源文件生成中的 C 代码。根据当前构建前提条件,3 种不同的方式安装:

    • 如果安装环境没有可用的 Cython,则扩展C代码是从提供的 Python/Cython 源中生成。
    • 如果 Cython 不可用,但有可用的构建前提条件(C 编译器,Python/C API 头),扩展是从分散式的预生成的 C 文件中构建。
    • 如果前面的先决条件都不可用,并且扩展是从纯 Python 源创建的,则模块将像普通 Python 代码一样安装,并跳过编译步骤。

    注意,包含生成的 C 文件以及 Cython 源文件,这是 Cython 文档中推荐的分发 Cython 扩展的方式。文档中还提到,默认情况下应该禁用 Cython 编译,因为用户在他的环境中可能没有所需的 Cython 版本,这可能会导致意想不到的编译问题。不过,随着环境隔离的出现,现今这似乎是一个不太令人担忧的问题。

  • Cython 作为一门语言。

    Cython 不仅是一个编译器,而且也是一个 Python 语言的超集。超集意味着任何有效的 Python 代码是允许的,并且它还具有一些额外的特性,如支持调用 C 函数或声明 C 类型的变量和类属性。所以任何用 Python 编写的代码都可以用作为 Cython 语言使用。这解释了为什么可以通过 Cython 编译器很容易地将普通的 Python 模块编译到 C。

    Cython 源文件使用不同的文件扩展名。它的扩展名是 .pyx 而不是 .py。

挑战

Python 是一个令人兴奋的语言,它有很多很酷的功能,可以用于很多领域。可读性很好,也容易写,然而易用性也伴随着一些代价。有很高的可移植性,但它的解释器无法运行在许多架构的用于其他语言的编译器上。

额外的复杂性

显而易见,使用不同语言的开发应用程序不是一件容易的事情。Python 和 C 是完全不同的技术,很难找到他们有什么共同点。需要额外的努力和时间在 C 和 Python 之间切换,或者一些额外的压力,最终会降低你的开发效率。

调试

在大多数情况下,扩展代码中的内存管理问题将会在 Python 中导致无法恢复的段错误,并且会导致解释器崩溃,并且不会抛出任何异常。这意味着你最终需要使用大多数 Python 程序员不需要使用的额外的工具。这增加了开发环境和工作流程的复杂性。

无扩展的动态库接口

由于 ctypes(标准库中的一个模块)或 cffi(一个外部包),你几乎可以在 Python 中集成任何一个编译的动态/共享库,无论这些库使用什么语言编写。

ctypes

ctypes 是最流行的模块,用于动态或共享库的函数调用,而不需要编写自定义 C 扩展。原因很明显。它是标准库的一部分,因此它始终可用,并且不需要任何外部依赖。它是一个外部函数接口(Foreign Function Interface, FFI)库,并提供了一个用于创建兼容 C 数据类型的 API。

  • 加载库。

    在 ctypes 中有 4 种类型的动态库加载器和两个使用它们的约定。表示动态和共享库的类是 ctypes.CDLL、ctypes.PyDLL、ctypes.OleDLL 和 ctypes.WinDLL。最后两个仅在 Windows 上可用。CDLL 和 PyDLL 之间的区别如下。

    • ctypes.CDLL:此类表示加载共享库。这些库中的函数使用标准调用约定,并假定返回int类型。GIL在调用期间释放。
    • ctypes.PyDLL:此类工作类似于CDLL,但 GIL 在调用期间不释放。执行后,将检查 Python 错误标志,如果设置了异常,就会抛出。它只在从 Python/C API 中直接调用函数时有用。

    要加载库,可以使用适当的参数实例化前面的一个类,或者从与特定类关联的子模块中调用 LoadLibrary() 函数。

    • 对于 ctypes.CDLL 使用 ctypes.cdll.LoadLibrary()。
    • 对于 ctypes.PyDLL 使用 ctypes.pydll.LoadLibrary()。
    • 对于 ctypes.WinDLL 使用 ctypes.windll.LoadLibrary()。
    • 对于 ctypes.OleDLL 使用 ctypes.oledll.LoadLibrary()。

    加载共享库的主要挑战是如何以便携式方式找到它们。不同的系统对共享库使用不同的后缀,并在不同的地方搜索它们。

  • 使用 ctypes 调用 C 函数。

    当库成功加载时,通用的模式是将其存储为与库名称相同的模块级变量。函数可以作为对象属性访问,因此调用它们就像调用从任何其他导入模块导入的 Python 函数一样。

    除了整数,字符串和字节之外的所有内置 Python 类型都与 C 数据类型不兼容,因此必须使用 ctypes 模块提供的相应类进行包装。

  • 传递 Python 函数作为 C 回调。

    是一个非常流行的设计模式,将函数实现的部分工作委托给用户提供的自定义回调。在 C 标准库中,最著名的接受这种回调的函数是 qsort() 函数,它提供了 Quicksort 算法的通用实现。你不太可能会直接使用这种算法,而是使用更适合 Python 集合排序的默认 Python Timsort 排序方法。

    普通的 Python 函数类型与 qsort() 规范所需的回调函数类型不兼容。qsort() 的签名,它还包含接受的回调类型(compare 参数)的类型。

CFFI

CFFI 是 Python 的外部函数接口,是 ctypes 的一个替代品。它不是标准库的一部分,但是它作为一个 cffi 包,可以很容易地从 PyPI 上获得。它的方式更复杂,并且还有一个特性,它允许你使用 C 编译器将集成层的某些部分自动编译为扩展。因此,它可以用作填补 C 扩展和 ctypes 之间差距的混合解决方案。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值