使用 Python 作为胶水#

警告

此内容撰写于 2008 年,作为 Travis E. Oliphant 撰写的原始 NumPy 指南 书籍的一部分,并且已过期。

世界上最无聊的对话莫过于每个人
都表示同意。
米歇尔·德·蒙田
胶带就像原力。它有两面性,既光明又黑暗,
它将宇宙连接在一起。
卡尔·茨万西格

许多人喜欢说 Python 是一种奇妙的胶水语言。希望本章会让你信服。最早采用科学 Python 的人通常是用它来粘合运行在超级计算机上的大型应用程序代码。不仅用 Python 编写代码比用外壳脚本或 Perl 好得多,此外,轻松扩展 Python 的能力还可以相对轻松地创建专门适用于正在解决的问题的新类和类型。从这些早期贡献者的互动中,Numeric 作为一个类数组对象出现,可用于在这些应用程序之间传递数据。

随着 Numeric 成熟并发展为 NumPy,人们已经能够直接在 NumPy 中编写更多代码。通常此代码足够快,可用于生产用途,但是还是有时需要访问已编译代码。或者是为了从算法中获得最后的效率,或者为了更轻松地访问用 C/C++ 或 Fortran 编写的广泛可用的代码。

本章将回顾许多可用于访问其他编译型语言中编写代码的工具。有许多资源可以用来学习如何从 Python 中调用其他已编译库,而本章的目的并非使您成为专家。主要目标是让您了解一些可能性,以便您知道要如何“谷歌搜索”才能了解更多信息。

从 Python 中调用其他已编译库#

虽然 Python 是一种绝佳的语言且编码也很有趣,但它的动态本质会导致开销,这可能使某些代码( for 循环内的原始计算)比以静态编译型语言编写的等效代码慢 10-100 倍。此外,它还会导致内存使用量比必需的大,因为会在计算期间创建和销毁临时数组。对于许多类型的计算需求,通常无法节省额外的减速和内存消耗(至少对于代码中时间或内存关键的部分是如此)。因此,最常见需求之一是从 Python 代码调用快速机器代码例程(例如使用 C/C++ 或 Fortran 编译)。相对容易做到这一点是 Python 之所以成为科学和工程编程优秀的高级语言的一个重要原因。

调用编译代码有两种基本方法:编写扩展模块,然后使用 import 命令导入到 Python 中,或者使用 ctypes 模块直接从 Python 调用共享库子程序。编写扩展模块是最常见的方法。

警告

如果不小心,从 Python 中调用 C 代码会导致 Python 崩溃。本章中的任何方法都不能避免这一点。您必须了解 NumPy 和所用第三方库处理数据的方式。

手工生成的包装器#

编写扩展模块 中讨论了扩展模块。与编译代码连接的最基本方法是编写一个扩展模块然后构造一个调用编译代码的模块方法。为了提高可读性,您的方法应利用 PyArg_ParseTuple调用在 Python 对象和 C 数据类型之间进行转换。对于标准 C 数据类型,可能已经有一个内置转换器。对于其他类型,可能需要编写自己的转换器并使用 "O&" 格式字符串,该字符串允许您指定一个函数,用于执行从 Python 对象到所需的任何 C 结构的转换。

一但完成了适当的 C 结构和 C 数据类型的转换,封装程序中的下一步就该调用底层函数了。如果底层函数使用的是 C 或 C++,那这件事很容易。然而,如果你想要调用 Fortran 代码,你必须熟悉使用你的编译器和平台在 C/C++ 中调用 Fortran 子例程的方法。这可能会因平台和编译器的不同而有所区别(这也是 f2py 能极大地简化 Fortran 代码的接口的另一个原因),但通常会涉及到名称的下划线转换和所有变量都通过引用传递的事实(即所有参数都是指针)。

手工生成的封装程序的优势在于你可以完全控制 C 库的被使用和调用方式,从而形成精简、紧凑的、开销最小的接口。其劣势在于你不得不编写、调试和维护 C 代码,虽然它的大部分内容都可以使用在其他扩展模块中久经考验的“剪切-粘贴-修改”技术来进行改编。由于调用其他 C 代码的过程是相当固定的,因此已经开发出了代码生成过程来简化该过程。其中一种代码生成技术随着 NumPy 一起分发,并允许轻松集成 Fortran 和(简单的)C 代码。程序包 f2py 将在下个部分中进行简要介绍。

F2PY#

F2PY 让你能够自动构造一个扩展模块,该模块可以和 Fortran 77/90/95 代码中的例程进行接口。它能够解析 Fortran 77/90/95 代码并自动生成它遇到的子例程的 Python 签名,或者你也可以通过构建接口定义文件(或修改 f2py 生成的文件)来指导子例程如何与 Python 进行接口。

查看F2PY 文档了解更多信息和示例。

f2py 方法链接编译代码的方式目前是最复杂和集成的方法。它允许将 Python 与编译代码明确分离开来,同时仍然允许单独分发扩展模块。唯一的缺点是它要求用户在安装代码之前必须有一个 Fortran 编译器。然而,随着免费编译器 g77、gfortran 和 g95 的诞生以及高质量的商用编译器的出现,这种限制并没有特别烦人。我们认为,Fortran 仍然是为科学计算编写快速、清晰的代码最简单的方法。它以最直接的方式处理复数和多维索引。但是,你要知道某些 Fortran 编译器无法像优秀的人工编写的 C 代码一样优化代码。

Cython#

Cython 是一个 Python 方言编译器,它添加了(可选的)静态类型以提高速度,并且允许在你的模块中混合使用 C 或 C++ 代码。它生成 C 或 C++ 扩展,这些扩展可以在 Python 代码中进行编译和导入。

如果您正在编写一个扩展模块,它还将包含您自己的相当一部分算法代码,那么 Cython 将是一个不错的选择。在它的众多特色中,包括可以轻松快速地使用多维数组的能力。

请注意,Cython 只是一个扩展模块生成器。它不同于 f2py,它不包含自动编译和链接扩展模块(这必须按照常规方式完成)的工具。它确实提供了一个名为 build_ext 的已修改的 distutils 类,它让您可以从 .pyx 源代码构建一个扩展模块。因此,您可以在 setup.py 文件中编写

from Cython.Distutils import build_ext
from distutils.extension import Extension
from distutils.core import setup
import numpy

setup(name='mine', description='Nothing',
      ext_modules=[Extension('filter', ['filter.pyx'],
                             include_dirs=[numpy.get_include()])],
      cmdclass = {'build_ext':build_ext})

当然,只有当您在扩展模块(我们假设您使用 Cython 的原因)中使用 NumPy 数组时,才需要添加 NumPy 包含目录。NumPy 中的 distutils 扩展还包括自动生成扩展模块并从 .pyx 文件将其链接起来的支持。它的工作原理是,如果用户未安装 Cython,它将查找一个具有相同文件名但扩展名为 .c 的文件,然后使用它而不是尝试再次生成 .c 文件。

如果您只使用 Cython 来编译标准 Python 模块,那么您将获得一个通常运行速度比等效 Python 模块稍快的 C 扩展模块。通过使用 cdef 关键字来静态定义 C 变量,可以获得进一步的速度提升。

让我们看看我们之前见过的两个示例,看看如何使用 Cython 实施它们。这些示例使用 Cython 0.21.1 编译成扩展模块。

Cython 中的复数加法#

以下是 Cython 模块的一部分,该模块名为 add.pyx,它实现了我们之前使用 f2py 实现的复数加法函数

cimport cython
cimport numpy as np
import numpy as np

# We need to initialize NumPy.
np.import_array()

#@cython.boundscheck(False)
def zadd(in1, in2):
    cdef double complex[:] a = in1.ravel()
    cdef double complex[:] b = in2.ravel()

    out = np.empty(a.shape[0], np.complex64)
    cdef double complex[:] c = out.ravel()

    for i in range(c.shape[0]):
        c[i].real = a[i].real + b[i].real
        c[i].imag = a[i].imag + b[i].imag

    return out

此模块展示了使用 cimport 语句从 Cython 附带的 numpy.pxd 头文件中加载定义。看起来 NumPy 被导入了两次;cimport 仅使 NumPy C-API 可用,而常规的 import 导致在运行时以 Python 风格进行导入,并使调用熟悉的 NumPy Python API 成为可能。

该示例还说明了 Cython 的“类型化内存视图”,它就像 C 级的 NumPy 数组,从其具有自己已知的扩展(不像通过裸指针寻址的 C 数组)。语法 double complex[:] 表明一个任意跨度的双精度浮点数一维数组(向量)。一个紧凑的 int 数组将是 int[::1],而浮点数矩阵将是 float[:, :]

所示注解是 cython.boundscheck 装饰器,它将基于每个函数开关内存视图访问的边界检查。我们可以使用此功能在牺牲安全性(或在进入循环之前进行手动检查)的情况下进一步加速我们的代码。

除了视图语法之外,该函数对 Python 程序员而言即可读懂。变量 i 的静态类型化是隐式的。除了视图语法之外,我们还可以使用 Cython 特殊的 NumPy 数组语法,但视图语法更受青睐。

Cython 中的图像滤镜#

我们使用 Fortran 创建的二维示例在 Cython 中也很容易编写

cimport numpy as np
import numpy as np

np.import_array()

def filter(img):
    cdef double[:, :] a = np.asarray(img, dtype=np.double)
    out = np.zeros(img.shape, dtype=np.double)
    cdef double[:, ::1] b = out

    cdef np.npy_intp i, j

    for i in range(1, a.shape[0] - 1):
        for j in range(1, a.shape[1] - 1):
            b[i, j] = (a[i, j]
                       + .5 * (  a[i-1, j] + a[i+1, j]
                               + a[i, j-1] + a[i, j+1])
                       + .25 * (  a[i-1, j-1] + a[i-1, j+1]
                                + a[i+1, j-1] + a[i+1, j+1]))

    return out

此二维平均滤镜运行得很快,因为循环在 C 中,仅在需要时执行指针运算。如果将上述代码编译为模块 image,则可以非常快速地使用此代码滤镜化二维图像 img

import image
out = image.filter(img)

关于代码,有两点值得注意:首先,不可能将内存视图返回到 Python。相反,首先必须创建一个 NumPy 数组 out,然后将此数组上的视图 b 用于计算。其次,视图 b 的类型是 double[:, ::1]。这意味着带有紧凑行(即 C 矩阵顺序)的二维数组。显式指定顺序可以加速某些算法,因为它们可以跳过跨度计算。

结论#

Cython 是多个科学 Python 库(包括 Scipy、Pandas、SAGE、scikit-image 和 scikit-learn)以及 XML 处理库 LXML 的首选扩展机制。该语言和编译器维护得很好。

使用 Cython 有几个缺点

  1. 在对自定义算法进行编码时,有时在包装现有的 C 库时,需要一些 C 语言的理解。尤其在使用 C 内存管理 (malloc 和类似内容) 时,很容易出现内存泄漏。但是,只需将已重命名的 Python 模块编译为 .pyx,即可提升其速度,并且通过添加少量类型声明,可以在某些代码中大幅提升速度。

  2. 很容易在 Python 和 C 之间丧失清晰的分隔,这会增加其他非 Python 相关项目中重新使用 C 代码的难度。

  3. Cython 生成的 C 代码难以阅读和修改(并且通常会编译出恼人但无害的警告信息)。

Cython 生成的扩展模块存在一个很大的优点,即它们易于分发。总之,Cython 是一款功能非常强大的工具,可用于粘合 C 代码或快速生成扩展模块,这种工具不被忽视,对于无法编写或不愿编写 C 或 Fortran 代码的人来说尤其有用。

ctypes#

ctypes 是 Python 扩展模块(包含在 stdlib 中),它让您可以直接从 Python 中调用共享库中的任意函数。此方法让您能够直接从 Python 中与 C 代码进行交互。这种方法为使用 Python 的各种库提供了可能。但是,缺点在于,编码错误很容易导致难看的程序崩溃(就像 C 中可能发生的情况一样),原因是几乎没有对参数进行类型或边界检查。当将数组数据传入一个指向裸内存位置的指针时,尤其如此。您需要负责该例程不会访问实际数组区域外部的内存。但是,如果您不介意小题大做,那么 ctypes 将成为快速利用大型共享库(或在您自己的共享库中编写扩展功能)的有效工具。

由于 ctypes 方法会将一个裸接口暴露给编译的代码,因此它并不总是能容忍用户错误。通常,健壮地使用 ctypes 模块包括附加一层 Python 代码,用于检查传递给底层例程的对象的数据类型和数组边界。此附加检查层(更不用说从 ctypes 对象到 C 数据类型的转换,这是 ctypes 自己执行的)会让该接口比手工编写的扩展模块接口更慢。但是,如果所调用的 C 例程要执行大量工作,此开销应可以忽略不计。如果您是一位优秀的 Python 程序员,但 C 技能较弱,那么 ctypes 是一种轻松的方法,可以编写出用于(共享)编译代码库的有用接口。

要使用 ctypes,您必须

  1. 拥有一个共享库。

  2. 加载共享库。

  3. 将 Python 对象转换为 ctypes 理解的参数。

  4. 使用 ctypes 参数从库中调用函数。

拥有共享库#

一个可用于 ctypes 的共享库有几项平台特定的要求。本指南假设您对在您的系统上制作共享库(或简单地拥有一个共享库)有一定了解。需要记住的项目有

  • 共享库必须以特殊方式编译(例如,使用 gcc 的 -shared 标志)。

  • 在某些平台(例如 Windows)上,共享库需要一个 .def 文件,用于指定要导出的函数。例如,mylib.def 文件可能包含

    LIBRARY mylib.dll
    EXPORTS
    cool_function1
    cool_function2
    

    或者,您可能能够在函数的 C 定义中使用存储类说明符 __declspec(dllexport) 来避免需要此 .def 文件。

在 Python distutils 中没有标准方法可以在跨平台的方式中创建一个标准共享库(扩展模块是一个 Python 理解的“特殊”共享库)。因此,在编写本书时 ctypes 的一个大缺点在于难以跨平台地分发使用 ctypes 的 Python 扩展,其中包含您自己的代码,这些代码应作为共享库编译在用户的系统上。

加载共享库#

一种简单但可靠的加载共享库的方法是获取绝对路径名称并使用 ctypes 的 cdll 对象加载它

lib = ctypes.cdll[<full_path_name>]

但是,在 Windows 上,访问 cdll 方法的属性将在当前目录或 PATH 中找到的第一个具有该名称的 DLL。加载绝对路径名称需要一些技巧用于跨平台工作,因为共享库的扩展是不同的。有一个 ctypes.util.find_library 实用程序,它可以简化查找要加载的库的过程,但它并不是万无一失。复杂的问题是,不同的平台共享库使用的默认扩展不同(例如 .dll – Windows,.so – Linux,.dylib – Mac OS X)。如果您使用 ctypes 封装需要在多个平台上工作的代码,则也必须考虑到这一点。

NumPy 提供了一个名为 ctypeslib.load_library (name, path) 的便捷函数。此函数采用共享库的名称(包括诸如“lib”的任何前缀但不包括扩展名)和可以找到共享库的路径。如果找不到该库,它将返回 ctypes 库对象或引发 OSError 异常,如果 ctypes 模块不可用,它将引发 ImportError 异常。(Windows 用户:使用 load_library 加载的 ctypes 库对象在加载时总是假定采用 cdecl 调用约定。请参阅 ctypes.windll 和/或 ctypes.oledll 中的 ctypes 文档,了解如何在其他调用约定下加载库)。

共享库中的函数可作为 ctypes 库对象的属性(由 ctypeslib.load_library 返回)使用,或作为项使用 lib['func_name'] 语法使用。如果函数名包含在 Python 变量名中不允许使用的字符,则检索函数名的上述方法非常有用。

转换参数#

Python int/long、string 和 unicode 对象会根据需要自动转换为相应的 ctypes 参数。None 对象也会自动转换为 NULL 指针。所有其他 Python 对象都必须转换为特定于 ctypes 的类型。有两种方法可以解决此限制,以便 ctypes 与其他对象集成。

  1. 不要设置函数对象的 argtypes 属性,并为要传入的对象定义一个 _as_parameter_ 方法。 _as_parameter_ 方法必须返回一个 Python int,该 int 将直接传递给该函数。

  2. 将 argtypes 属性设置为一个列表,其条目包含具有名为 from_param 的类方法的对象,该方法知道如何将对象转换为 ctypes 可以理解的对象(int/long、string、unicode 或具有 _as_parameter_ 属性的对象)。

NumPy 使用这两种方法,偏好第二种方法,因为它更安全。ndarray 的 ctypes 属性返回一个具有 _as_parameter_ 属性的对象,该属性返回一个代表与其关联的 ndarray 地址的整数。因此,可以将此 ctypes 属性对象直接传递给一个期望指向 ndarray 中数据的指针的函数。调用者必须确保 ndarray 对象具有正确的类型、形状并设置了正确的标志,否则如果将数据指针传入不当的数组,则可能会出现严重的崩溃。

为了实现第二个方法,NumPy 在 numpy.ctypeslib 模块中提供了类工厂函数 ndpointer。此类工厂函数生成一个合适的类,可以放入 ctypes 函数的 argtypes 属性项。此类将包含一个 from_param 方法,ctypes 将使用此方法将任何传递给该函数的 NDarray 转换为 ctypes 识别的对象。转换过程中将检查 ndpointer 函数调用时用户指定的 NDarray 任何属性。可检查的 NDarray 方面包括传入任何数组的数据类型、维度数、形状和/或标志状态。from_param 方法的返回值是数组的 ctypes 属性,由于它包含指向数组数据区的 _as_parameter_ 属性),ctypes 可直接使用它。

NDarray 的 ctypes 属性还具有其他属性,在将其他有关数组的信息传递到 ctypes 函数时可能会很方便。属性 **数据**、**形状** 和 **跨度** 可提供与数据区域、形状和数组跨度对应的 ctypes 兼容类型。数据属性返回 c_void_p,表示指向数据区域的指针。形状和跨度属性分别返回一个 ctypes 整数数组(或表示 NULL 指针的 None,如果是 0 维数组)。数组的基本 ctype 是与平台上的指针大小相同的 ctype 整数。还有 data_as({ctype})shape_as(<base ctype>)strides_as(<base ctype>) 方法。这些方法将数据作为您选择的 ctype 对象返回,将形状/跨度数组使用您选择的底层基本类型返回。为了方便起见,ctypeslib 模块还包含 c_intp 作为 ctypes 整数数据类型,其大小与平台上的 c_void_p 大小相同(如果未安装 ctypes,则其值为 None)。

调用函数 #

此函数可作为加载的共享库的属性或一项来访问。因此,如果 ./mylib.so 有一个名为 cool_function1 的函数,可以通过以下方式访问

lib = numpy.ctypeslib.load_library('mylib','.')
func1 = lib.cool_function1  # or equivalently
func1 = lib['cool_function1']

在 ctypes 中,函数的返回值默认设置为“int”。可以通过设置函数的 restype 属性来更改此行为。如果函数没有返回值(“void”),则使用 None 作为 restype

func1.restype = None

如前文所讨论的,还可以设置函数的 argtypes 属性,以便在调用函数时让 ctypes 检查输入参数的类型。使用 ndpointer 工厂函数来生成一个现成的类,以便对新函数中的数据类型、形状和标志进行检查。 ndpointer 函数有以下签名

ndpointer(dtype=None, ndim=None, shape=None, flags=None)#

None 的关键字参数不经过检查。指定一个关键字会强制在转换到与 ctypes 兼容的对象时检查 ndarray 的该方面。dtype 关键字可以是任何可以理解为数据类型对象的对象。ndim 关键字应当是整数,shape 关键字应当是整数或整数序列。flags 关键字指定传入的任何数组的最小标志。这可以指定为逗号分隔的要求字符串、指示所需位或在一起的整数,或者从具有必要的要求的数组的 flags 属性返回的 flags 对象。

在 argtypes 方法中使用 ndpointer 类可以让你使用 ctypes 和 ndarray 的数据区域来调用 C 函数更加安全。您可能仍希望使用额外的 Python 包装器来包装函数,以便它更用户友好(隐藏一些明显的参数并使一些参数成为输出参数)。在此过程中,NumPy 中的 requires 函数可能很有用,可以从给定的输入返回正确类型的数组。

完整示例#

在此示例中,我们将演示如何使用 ctypes 实现之前使用其他方法实现的加法函数和滤波函数。首先,实现这些算法的 C 代码包含函数 zadddaddsaddcadddfilter2dzadd 函数为

/* Add arrays of contiguous data */
typedef struct {double real; double imag;} cdouble;
typedef struct {float real; float imag;} cfloat;
void zadd(cdouble *a, cdouble *b, cdouble *c, long n)
{
    while (n--) {
        c->real = a->real + b->real;
        c->imag = a->imag + b->imag;
        a++; b++; c++;
    }
}

对于 cadddaddsadd 也有类似代码,它们分别处理复数浮点数、双精度浮点数和浮点数数据类型

void cadd(cfloat *a, cfloat *b, cfloat *c, long n)
{
        while (n--) {
                c->real = a->real + b->real;
                c->imag = a->imag + b->imag;
                a++; b++; c++;
        }
}
void dadd(double *a, double *b, double *c, long n)
{
        while (n--) {
                *c++ = *a++ + *b++;
        }
}
void sadd(float *a, float *b, float *c, long n)
{
        while (n--) {
                *c++ = *a++ + *b++;
        }
}

code.c 文件还包含函数 dfilter2d

/*
 * Assumes b is contiguous and has strides that are multiples of
 * sizeof(double)
 */
void
dfilter2d(double *a, double *b, ssize_t *astrides, ssize_t *dims)
{
    ssize_t i, j, M, N, S0, S1;
    ssize_t r, c, rm1, rp1, cp1, cm1;

    M = dims[0]; N = dims[1];
    S0 = astrides[0]/sizeof(double);
    S1 = astrides[1]/sizeof(double);
    for (i = 1; i < M - 1; i++) {
        r = i*S0;
        rp1 = r + S0;
        rm1 = r - S0;
        for (j = 1; j < N - 1; j++) {
            c = j*S1;
            cp1 = j + S1;
            cm1 = j - S1;
            b[i*N + j] = a[r + c] +
                (a[rp1 + c] + a[rm1 + c] +
                 a[r + cp1] + a[r + cm1])*0.5 +
                (a[rp1 + cp1] + a[rp1 + cm1] +
                 a[rm1 + cp1] + a[rm1 + cp1])*0.25;
        }
    }
}

此代码相对于 Fortran 等效代码的一个潜在优势是,它采用任意的跨距(即非连续数组),并且还可能根据编译器的优化能力运行得更快。但是,它明显比 filter.f 中的简单代码复杂。此代码必须编译成共享库。在我的 Linux 系统上,这是使用完成的

gcc -o code.so -shared code.c

这将在当前目录中创建一个名为 code.so 的共享库。在 Windows 上,别忘了在每个函数定义前一行中的 void 前面加上 __declspec(dllexport),或编写一个 code.def 文件,其中列出要导出的函数的名称。

应该为此共享库构建一个合适的 Python 接口。为此,创建一个名为 interface.py 的文件,并在文件的顶部添加以下行

__all__ = ['add', 'filter2d']

import numpy as np
import os

_path = os.path.dirname('__file__')
lib = np.ctypeslib.load_library('code', _path)
_typedict = {'zadd' : complex, 'sadd' : np.single,
             'cadd' : np.csingle, 'dadd' : float}
for name in _typedict.keys():
    val = getattr(lib, name)
    val.restype = None
    _type = _typedict[name]
    val.argtypes = [np.ctypeslib.ndpointer(_type,
                      flags='aligned, contiguous'),
                    np.ctypeslib.ndpointer(_type,
                      flags='aligned, contiguous'),
                    np.ctypeslib.ndpointer(_type,
                      flags='aligned, contiguous,'\
                            'writeable'),
                    np.ctypeslib.c_intp]

此代码加载与该文件位于同一路径中的名为 code.{ext} 的共享库。然后它向库中包含的函数添加 void 返回类型。它还向库中的函数添加参数检查,以便可以将 ndarray 传递为前三个参数,并将一个整数(大到足以在该平台上容纳指针)传递为第四个参数。

设置滤波函数也是类似的,它允许使用 ndarray 参数(作为前两个参数)和指向整数的指针(大到足以处理 ndarray 的跨距和形状)(作为最后两个参数)来调用滤波函数。

lib.dfilter2d.restype=None
lib.dfilter2d.argtypes = [np.ctypeslib.ndpointer(float, ndim=2,
                                       flags='aligned'),
                          np.ctypeslib.ndpointer(float, ndim=2,
                                 flags='aligned, contiguous,'\
                                       'writeable'),
                          ctypes.POINTER(np.ctypeslib.c_intp),
                          ctypes.POINTER(np.ctypeslib.c_intp)]

接下来,定义一个简单的选择函数,该函数根据数据类型选择在共享库中调用哪个加法函数

def select(dtype):
    if dtype.char in ['?bBhHf']:
        return lib.sadd, single
    elif dtype.char in ['F']:
        return lib.cadd, csingle
    elif dtype.char in ['DG']:
        return lib.zadd, complex
    else:
        return lib.dadd, float
    return func, ntype

最后,接口要导出的两个函数可以简单地写为

def add(a, b):
    requires = ['CONTIGUOUS', 'ALIGNED']
    a = np.asanyarray(a)
    func, dtype = select(a.dtype)
    a = np.require(a, dtype, requires)
    b = np.require(b, dtype, requires)
    c = np.empty_like(a)
    func(a,b,c,a.size)
    return c

def filter2d(a):
    a = np.require(a, float, ['ALIGNED'])
    b = np.zeros_like(a)
    lib.dfilter2d(a, b, a.ctypes.strides, a.ctypes.shape)
    return b

结论#

使用 ctypes 是连接 Python 和任意 C 代码的强大方式。它在扩展 Python 方面的优势包括

  • 干净地将 C 代码与 Python 代码分开

    • 除了 Python 和 C 以外,无需学习新的语法

    • 允许重复使用 C 代码

    • 通过简单的 Python 包装程序和搜索库,可以获取为其他目的编写的共享库中的功能。

  • 通过 ctypes 属性轻松地与 NumPy 集成

  • 使用 ndpointer 类工厂进行全面参数检查

它的缺点包括

  • 使用 ctypes 生成的扩展模块难以分发,因为在 distutils 中缺少对构建共享库的支持。

  • 你必须有代码的共享库(无静态库)。

  • 对 C++ 代码及其不同的库调用约定几乎没有支持。你可能需要在 C++ 代码周围的 C 包装器才能与 ctypes 一起使用(或只使用 Boost.Python)。

由于使用 ctypes 生成的扩展模块难以分发,因此 f2py 和 Cython 仍然是为程序包创建扩展 Python 的最简单方法。但是,在某些情况下,ctypes 是有用的替代方法。这应该为 ctypes 带来了更多特性,这些特性将消除使用 ctypes 扩展 Python 和分发扩展的难度。

你可能会觉得有用的其他工具#

这些工具被使用 Python 的其他人发现有用,因此在这里列出。它们被单独讨论,因为它们是现在由 f2py、Cython 或 ctypes (SWIG、PyFort)处理的旧方法,或者因为缺乏合理的文档(SIP、Boost)。此处未包含这些方法的链接,因为可以通过 Google 或其他搜索引擎找到最相关的链接,并且此处提供的任何链接都将很快过时。不要认为被列入本列表就意味着此程序包值得关注。在此收集这些程序包的信息,因为很多人发现它们很有用,我们希望为您提供尽可能多的选项,以解决轻松集成您代码的问题。

SWIG#

Simplified Wrapper and Interface Generator (SWIG) 是一种古老且较为稳定的方法,用于将 C/C++ 库包装成其他多种语言。它不专门理解 NumPy 数组,但可以通过使用 typemap 在 NumPy 中使其可用。numpy.i 下的 numpy/tools/swig 目录中有一些示例 typemap,以及一个利用它们的示例模块。SWIG 擅长包装大型 C/C++ 库,因为它可以(几乎)解析这些库的标头,并自动生成一个接口。从技术上讲,你需要生成一个定义该接口的 .i 文件。然而,通常,这个 .i 文件也可以是该标头本身的一部分。该接口通常需要进行一些调整才能变得非常有用。解析 C/C++ 标头和自动生成接口的能力使 SWIG 仍然成为将 C/C++ 的功能添加到 Python 中的一种有用的方法,尽管后来出现其他更针对 Python 的方法。SWIG 实际上可以针对多种语言的扩展,但 typemap 通常必须针对特定语言。尽管如此,通过对特定于 Python 的 typemap 进行修改,可以使用 SWIG 将库与其他语言(例如 Perl、Tcl 和 Ruby)进行接口。

我对 SWIG 的经验整体上是正面的,因为它相对容易使用并且功能强大。它经常在变得更精通编写 C 扩展之前使用。然而,用 SWIG 编写自定义接口通常很麻烦,因为它必须使用 typemap 的概念来完成,typemap 不是特定于 Python 的,并且是用类似 C 语法的语言编写的。因此,更青睐其他粘合策略,而 SWIG 可能只会考虑包装非常大的 C/C++ 库。尽管如此,其他人很乐于使用 SWIG。

SIP#

SIP 是另一种包装 C/C++ 库的工具,它特定于 Python,并且似乎对 C++ 有很好地支持。Riverbank Computing 开发了 SIP,以便为 QT 库创建 Python 绑定。必须编写一个接口文件来生成绑定,但该接口文件看起来很像 C/C++ 标头文件。虽然 SIP 不是一个完整的 C++ 编译器,但它理解相当多的 C++ 语法,以及其自己的特殊指令,这些指令允许修改 Python 绑定的实现方式。它还允许用户定义 Python 类型和 C/C++ 结构与类之间的映射。

Boost Python#

Boost 是 C++ 库的存储库,Boost.Python 便是其中一个库,它为将 C++ 类与函数绑定到 Python 提供了一个简洁的界面。Boost.Python 方法很神奇,因为它完全在纯 C++ 中执行,无需引入新语法。许多 C++ 用户表示 Boost.Python 使得无缝结合两个世界的优点成为了可能。使用 Boost 来封装简单的 C 子例程通常有些杀鸡用牛刀。它的主要目的是让 C++ 类在 Python 中可用。因此,如果您有一组需要干净集成到 Python 中的 C++ 类,请考虑了解和使用 Boost.Python。

Pyfort#

Pyfort 是一款优秀的工具,可将 Fortran 以及类似 Fortran 的 C 代码封装到 Python 中,并支持 Numeric 数组。它是计算机科学家保罗·杜波依斯编写,他是 Numeric 的第一个维护者(现已退休)。值得一提的是,希望有人将 PyFort 更新,使其也可用于 NumPy 数组,它们现在支持 Fortran 或 C 风格的连续数组。