使用Python作为粘合剂#

警告

这写于2008年,是Travis E. Oliphant的原始NumPy指南的一部分,已过时。

没有什么比所有人都#
同意的对话更无聊了。
Michel de Montaigne
管道胶带就像原力。它有光明面和黑暗面,并且
它将宇宙维系在一起。
Carl Zwanzig

很多人喜欢说Python是一种出色的粘合语言。希望本章能让您相信这是真的。早期使用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++代码混合到您的模块中。它生成C或C++扩展,这些扩展可以被编译并在Python代码中导入。

如果您正在编写包含大量自定义算法代码的扩展模块,那么Cython是一个不错的选择。它的功能之一是可以轻松快速地处理多维数组。

请注意,Cython仅是扩展模块生成器。与f2py不同,它不包含编译和链接扩展模块的自动功能(这必须以通常方式完成)。它提供了一个修改后的distutils类,称为build_ext,它允许您从.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包含目录仅在您在扩展模块中使用NumPy数组时才需要(这正是我们假设您使用Cython的原因)。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]。这意味着2D数组具有连续的行,即C矩阵顺序。显式指定顺序可以加快某些算法的速度,因为它们可以跳过步长计算。

结论#

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

使用Cython有几个缺点。

  1. 在编写自定义算法时,有时在包装现有C库时,需要熟悉C。特别是,在使用C内存管理(malloc及其相关函数)时,很容易引入内存泄漏。然而,只需将Python模块重命名为.pyx进行编译就可以加快速度,而添加一些类型声明可以在某些代码中带来显著的速度提升。

  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
    

    或者,您可以使用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文档中的ctypes.windll和/或ctypes.oledll,了解在其他调用约定下的加载库的方法)。

共享库中的函数可作为ctypes库对象(从ctypeslib.load_library返回)的属性或使用lib['func_name']语法进行访问。后一种检索函数名称的方法特别有用,如果函数名称包含不允许在Python变量名中使用的字符。

转换参数#

Python的int/long、字符串和unicode对象会根据需要自动转换为等效的ctypes参数。None对象也会自动转换为NULL指针。所有其他Python对象都必须转换为ctypes特定类型。有两种方法可以绕过此限制,使ctypes能够与其他对象集成。

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

  2. 将argtypes属性设置为一个列表,该列表的条目包含具有名为from_param的类方法的对象,该方法知道如何将您的对象转换为ctypes可理解的对象(int/long、字符串、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整数数组(如果数组为0-d,则返回None表示NULL指针)。数组的基本ctype是与平台上指针大小相同的ctypes整数。还有方法data_as({ctype})shape_as(<base ctype>)strides_as(<base ctype>)。它们返回您选择的ctypes对象形式的数据,以及使用您选择的底层基本类型的形状/步长数组。为方便起见,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的关键字参数不被检查。指定关键字会强制检查ndarray在转换为ctypes兼容对象时的该方面。dtype关键字可以是任何被理解为数据类型对象的对象。ndim关键字应该是整数,shape关键字应该是整数或整数序列。flags关键字指定传递给任何数组的最小必需标志。这可以指定为逗号分隔的需求字符串、指示需求位 OR 运算的整数,或者来自具有必要需求的数组的flags属性返回的flags对象。

在argtypes方法中使用ndpointer类可以显著提高使用ctypes和ndarray的数据区域调用C函数的安全性。您仍然可能希望将函数包装在额外的Python包装器中,以使其用户友好(隐藏一些显而易见的参数并将一些参数作为输出参数)。在此过程中,NumPy中的requires函数可能有助于从给定输入返回正确的数组。

完整示例#

在此示例中,我们将演示如何使用ctypes实现先前使用其他方法实现的加法函数和过滤器函数。首先,实现算法的C代码包含zadddaddsaddcadddfilter2d函数。zadd函数是

/* 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 带来更多功能,从而消除扩展 Python 和使用 ctypes 分发扩展的难度。

您可能觉得有用的附加工具#

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

SWIG#

Simplified Wrapper and Interface Generator (SWIG) 是一种较老且相当稳定的方法,用于将 C/C++ 库包装到各种其他语言中。它不专门理解 NumPy 数组,但可以通过使用类型映射(typemaps)使其与 NumPy 一起使用。在 numpy/tools/swig 目录下,与 numpy.i 一起,有一些示例类型映射,以及一个利用它们的示例模块。SWIG 在包装大型 C/C++ 库方面表现出色,因为它(几乎)可以解析它们的头文件并自动生成接口。技术上,您需要生成一个定义接口的 .i 文件。然而,通常这个 .i 文件可以是头文件本身的一部分。接口通常需要一些调整才能非常有用。尽管出现了其他更针对 Python 的方法,但这种解析 C/C++ 头文件并自动生成接口的能力仍然使 SWIG 成为将 C/C++ 功能添加到 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 数组。它由 Paul Dubois 编写,他是一位杰出的计算机科学家,也是 Numeric(现已退休)的第一任维护者。值得一提的是,希望有人能够更新 PyFort 以便也能与 NumPy 数组一起工作,而 NumPy 数组现在支持 Fortran 或 C 风格的连续数组。