使用Python作为粘合剂#

警告

本文撰写于2008年,作为Travis E. Oliphant撰写的原版NumPy指南书籍的一部分,现已过时。

世界上最无聊的谈话莫过于
人人意见一致的谈话。
蒙田
胶带就像原力,它有光明面和黑暗面,并且
它将宇宙维系在一起。
卡尔·兹万齐格

许多人喜欢说Python是一种很棒的粘合语言。希望本章能让你相信这是真的。最早将Python用于科学研究的人通常是那些使用它来将运行在超级计算机上的大型应用程序代码粘合在一起的人。它不仅比shell脚本或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++代码。它生成可以在Python代码中编译和导入的C或C++扩展。

如果你正在编写一个扩展模块,该模块将包含相当多的你自己的算法代码,那么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})

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

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

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

Cython中的复数加法#

这是名为add.pyx的Cython模块的一部分,它实现了我们之前使用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[::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 及其相关函数)时,很容易引入内存泄漏。但是,只需编译一个重命名为.pyx的 Python 模块就可以加快其速度,并且添加一些类型声明可以在某些代码中实现显著的加速。

  2. 很容易丢失 Python 和 C 之间的清晰分离,这使得将你的 C 代码重用于其他与 Python 无关的项目更加困难。

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

Cython 生成的扩展模块的一个巨大优势是它们易于分发。总而言之,Cython 是一个非常强大的工具,可以用来粘合 C 代码或快速生成扩展模块,不应该被忽视。对于无法或不愿编写 C 或 Fortran 代码的人来说,它尤其有用。

ctypes#

ctypes 是一个 Python 扩展模块,包含在标准库中,允许你直接从 Python 调用共享库中的任意函数。这种方法允许你直接从 Python 与 C 代码交互。这为 Python 开放了大量的库。但是,缺点是编码错误很容易导致程序崩溃(就像在 C 中一样),因为对参数进行的类型或边界检查很少。当数组数据作为指向原始内存位置的指针传递时,尤其如此。那么你的责任就是子程序不会访问实际数组区域之外的内存。但是,如果你不介意冒险一点,ctypes 可以成为一个有效的工具,可以快速利用大型共享库(或在你自己的共享库中编写扩展功能)。

因为 ctypes 方法向编译后的代码公开了一个原始接口,所以它并不总是容忍用户错误。稳健地使用 ctypes 模块通常需要一个额外的 Python 代码层来检查传递给底层子程序的对象的数据类型和数组边界。这额外的检查层(更不用说 ctypes 本身执行的从 ctypes 对象到 C 数据类型的转换),将使接口比手工编写的扩展模块接口慢。但是,如果调用的 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
    

    或者,你可能可以使用存储类说明符__declspec(dllexport)在函数的 C 定义中避免需要此.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 整数/长整数、字符串和 Unicode 对象会根据需要自动转换为等效的 ctypes 参数。None 对象也会自动转换为 NULL 指针。所有其他 Python 对象必须转换为 ctypes 特定的类型。有两种方法可以解决此限制,允许 ctypes 与其他对象集成。

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

  2. 将 argtypes 属性设置为一个列表,该列表的条目包含一个名为 from_param 的类方法的对象,该方法知道如何将你的对象转换为 ctypes 可以理解的对象(一个整数/长整数、字符串、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 函数时,这些属性可能很方便。属性 **data**、**shape** 和 **strides** 可以提供与数组的数据区域、形状和步幅相对应的 ctypes 兼容类型。`data` 属性返回一个 c_void_p,表示指向数据区域的指针。`shape` 和 `strides` 属性分别返回 ctypes 整数数组(或表示 NULL 指针的 None,如果为 0 维数组)。数组的基本 ctypes 类型是与平台上的指针大小相同的 ctypes 整数。还有方法 data_as({ctype})shape_as(<base ctype>)strides_as(<base ctype>)。这些方法将数据作为您选择的 ctype 对象返回,并使用您选择的底层基本类型返回 shape/strides 数组。为方便起见,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”),则对 `restype` 使用 None。

func1.restype = None

如前所述,您还可以设置函数的 `argtypes` 属性,以便在调用函数时让 ctypes 检查输入参数的类型。使用 ndpointer 工厂函数为您的新函数生成一个用于数据类型、形状和标志检查的现成类。ndpointer 函数的签名如下:

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

值为 None 的关键字参数不会被检查。指定关键字将强制检查转换为 ctypes 兼容对象的 ndarray 的该方面。`dtype` 关键字可以是任何被理解为数据类型对象的任何对象。`ndim` 关键字应为整数,`shape` 关键字应为整数或整数序列。`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 上,不要忘记在每个函数定义之前的行前面添加 __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 返回类型添加到库中包含的函数。它还向库中的函数添加了参数检查,以便可以将 ndarrays 作为前三个参数传递,以及一个整数(足够大以容纳平台上的指针)作为第四个参数。

滤波函数的设置类似,并允许使用 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 类工厂进行完整的参数检查

它的缺点包括:

  • 由于 distutils 缺乏对构建共享库的支持,因此很难分发使用 ctypes 编写的扩展模块。

  • 您必须拥有代码的共享库(没有静态库)。

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

由于使用 ctypes 编写的扩展模块难以分发,因此 f2py 和 Cython 仍然是扩展 Python 以创建软件包的最简单方法。但是,ctypes 在某些情况下是一个有用的替代方案。这应该为 ctypes 带来更多功能,这些功能应该能够消除扩展 Python 和使用 ctypes 分发扩展的难度。

您可能会发现有用的其他工具#

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

SWIG#

简化包装器和接口生成器 (SWIG) 是一种古老且相当稳定的方法,用于将 C/C++ 库包装到各种其他语言。它不专门理解 NumPy 数组,但可以通过使用类型映射使其与 NumPy 可用。在 numpy/tools/swig 目录下的 numpy.i 中有一些示例类型映射,以及一个使用它们的示例模块。SWIG 擅长包装大型 C/C++ 库,因为它可以(几乎)解析其头文件并自动生成接口。从技术上讲,您需要生成一个.i文件来定义接口。但是,通常情况下,这个.i文件可以是头文件的一部分。接口通常需要一些调整才能非常有用。这种解析 C/C++ 头文件并自动生成接口的能力仍然使 SWIG 成为一种将 C/C++ 功能添加到 Python 的有用方法,尽管出现了其他更针对 Python 的方法。SWIG 实际上可以针对多种语言的扩展,但类型映射通常必须是特定于语言的。尽管如此,通过修改特定于 Python 的类型映射,SWIG 可用于将库与其他语言(如 Perl、Tcl 和 Ruby)连接。

我使用 SWIG 的经验总体上是积极的,因为它相对易于使用且功能强大。在更熟练地编写 C 扩展之前,它经常被使用。但是,使用 SWIG 编写自定义接口通常很麻烦,因为它必须使用类型映射的概念来完成,而类型映射不是特定于 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(现已退休)的第一位维护者 Paul Dubois 编写。值得一提的是,希望有人会更新 PyFort 以使其也能与 NumPy 数组一起工作,NumPy 数组现在支持 Fortran 或 C 样式的连续数组。