使用 Python 作为“胶水”#
警告
此内容编写于 2008 年,是 Travis E. Oliphant 所著的原始《NumPy 指南》一书的一部分,现已过时。
许多人喜欢说 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++ 代码混合到你的模块中。它生成可编译并在 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 的“类型化内存视图”(typed memoryviews),它们在 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 有几个缺点
在编写自定义算法时,有时在包装现有 C 库时,需要对 C 有一定的熟悉。特别是,在使用 C 内存管理(
malloc
及相关函数)时,很容易引入内存泄漏。然而,仅仅将一个 Python 模块重命名为.pyx
并编译它就可以使其加速,添加一些类型声明可以在某些代码中带来显著的加速效果。Python 和 C 之间的清晰分离容易丢失,这使得你的 C 代码在其他非 Python 相关项目中重用变得更加困难。
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,你必须
拥有一个共享库。
加载共享库。
将 Python 对象转换为 ctypes 可理解的参数。
使用 ctypes 参数调用库中的函数。
转换参数#
Python 的 int/long、字符串和 unicode 对象会根据需要自动转换为等效的 ctypes 参数。None 对象也会自动转换为 NULL 指针。所有其他 Python 对象必须转换为 ctypes 特定的类型。有两种方法可以绕过这个限制,允许 ctypes 与其他对象集成。
不要设置函数对象的 argtypes 属性,而是为你想要传入的对象定义一个
_as_parameter_
方法。_as_parameter_
方法必须返回一个 Python 整数,该整数将直接传递给函数。将 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 维数组,则为 None 表示 NULL 指针)。数组的基本 ctype 是一个与平台上指针大小相同的 ctypes 整数。还有方法 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’),则 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 关键字指定了传入的任何数组所需的最小标志。这可以指定为逗号分隔的需求字符串,一个表示需求位进行 OR 运算的整数,或者一个从数组的 flags 属性返回的具有必要需求的 flags 对象。
在 argtypes 方法中使用 ndpointer 类可以显著提高使用 ctypes 和 ndarray 的数据区域调用 C 函数的安全性。你可能仍然希望将该函数封装在一个额外的 Python 包装器中,以使其更用户友好(隐藏一些明显的参数并将某些参数设为输出参数)。在此过程中,NumPy 中的 requires
函数可能有助于从给定输入返回正确类型的数组。
完整示例#
在此示例中,我们将演示之前使用其他方法实现的加法函数和滤镜函数如何使用 ctypes 实现。首先,实现这些算法的 C 代码包含函数 zadd
、dadd
、sadd
、cadd
和 dfilter2d
。其中 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++;
}
}
以及针对 cadd
、dadd
和 sadd
的类似代码,它们分别处理复数浮点型、双精度浮点型和浮点型数据类型
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 类工厂进行完整的参数检查
其缺点包括
由于 distutils 中缺乏对构建共享库的支持,因此难以分发使用 ctypes 创建的扩展模块。
你的代码必须是共享库(而不是静态库)。
对 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 数组,但可以通过使用类型映射(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 风格的连续数组。