使用 Python 作为粘合剂#

警告

这是作为 Travis E. Oliphant 的原始 Guide to NumPy 书的一部分于2008年写的,现已过时.

没有比每个人都参与的对话更无聊的了.
同意.
米歇尔·德·蒙田
胶带就像原力.它有光明的一面,也有黑暗的一面,而且
它将宇宙维系在一起.
Carl Zwanzig

许多人喜欢说Python是一种极好的胶水语言.希望这一章能让你相信这是真的.科学领域中最早采用Python的人通常使用它来将运行在超级计算机上的大型应用程序代码粘合在一起.不仅用Python编码比用shell脚本或Perl更舒适,而且,轻松扩展Python的能力使得创建专门适应所解决问题的新的类和类型相对容易.从这些早期贡献者的互动中,Numeric作为一种类似数组的对象出现,可以用于在这些应用程序之间传递数据.

随着 Numeric 成熟并发展成为 NumPy,人们已经能够在 NumPy 中直接编写更多代码.通常这些代码在生产环境中已经足够快,但有时仍然需要访问编译代码.无论是为了从算法中获得最后一点效率,还是为了更容易地访问用 C/C++ 或 Fortran 编写的广泛可用的代码.

本章将回顾许多可用于访问用其他编译语言编写的代码的工具.有许多资源可用于学习从Python调用其他编译库,本章的目的不是让你成为专家.主要目标是让你了解一些可能性,以便你知道要”Google”什么来学习更多.

从 Python 调用其他编译库#

虽然Python是一种很棒的语言,编写代码是一种乐趣,但其动态特性会导致一些代码(*即*在循环内的原始计算)比用静态编译语言编写的等效代码慢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 数组时,才需要添加 NumPy 包含目录(这也是我们假设你使用 Cython 的原因).NumPy 中的 distutils 扩展还包括自动生成扩展模块并从 .pyx 文件链接它的支持.它的工作原理是,如果用户没有安装 Cython,那么它会查找一个文件名相同但扩展名为 .c 的文件,然后使用该文件,而不是再次尝试生成 .c 文件.

如果你只是使用 Cython 来编译一个标准的 Python 模块,那么你将得到一个通常比等效的 Python 模块运行得稍快的 C 扩展模块.通过使用 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 及其相关函数)时,很容易引入内存泄漏.然而,仅仅将Python模块重命名为 .pyx 就可以加速编译,并且在某些代码中添加一些类型声明可以显著提高速度.

  2. 在Python和C之间很容易失去清晰的分离,这使得为其他非Python相关项目重用你的C代码变得更加困难.

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

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

转换参数#

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

  1. 不要设置函数对象的 argtypes 属性,并为你要传入的对象定义一个 _as_parameter_ 方法.``_as_parameter_`` 方法必须返回一个 Python 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 函数时可能很方便.属性 datashapestrides 可以提供与数据区域、形状和数组步幅相对应的 ctypes 兼容类型.data 属性返回一个表示指向数据区域指针的 c_void_p.shape 和 strides 属性各自返回一个 ctypes 整数数组(或者如果是一个 0-d 数组,则返回表示 NULL 指针的 None).数组的基本 ctype 是与平台上的指针大小相同的 ctype 整数.还有方法 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 工厂函数为您的函数生成一个现成的类,用于数据类型、形状和标志检查.:func: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 代码包含函数 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 上,不要忘记在每个函数定义前的行前面添加 __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 中的有用方法.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 编写,Paul Dubois 是一位杰出的计算机科学家,也是 Numeric(现已退休)的首位维护者.值得一提的是,希望有人会更新 PyFort,使其也能与支持 Fortran 或 C 风格连续数组的 NumPy 数组一起工作.