将 Python 用作胶水#

警告

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

没有什么对话比每个人都
同意的对话更无聊了.
— Michel de Montaigne
管道胶带就像原力.它有光明的一面,也有黑暗的一面,而且
它把宇宙连接在一起.
— Carl Zwanzig

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

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

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

从 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 documentation .

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 数组时(这就是我们假设您使用 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 之间的清晰分离,这使得为其他非 Python 相关的项目重复使用你的 C 代码更加困难.

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

Cython 生成的扩展模块的一个很大的优点是它们易于分发. 总之,Cython 是一个非常强大的工具,可以快速粘合 C 代码或生成扩展模块,不应被忽视. 它对于那些不能或不想编写 C 或 Fortran 代码的人特别有用.

ctypes#

ctypes 是一个 Python 扩展模块,包含在 stdlib 中,它允许你直接从 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,该 int 将直接传递给函数.

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

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

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

ndarray 的 ctypes 属性还被赋予了额外的属性,当将关于数组的附加信息传递到 ctypes 函数中时,这些属性可能会很方便.属性 data , shapestrides 可以提供与数组的数据区域,形状和步长相对应的 ctypes 兼容类型. data 属性返回一个表示指向数据区域的指针的 c_void_p . shapestrides 属性各自返回一个 ctypes 整数数组(如果数组为 0 维,则返回 None ,表示 NULL 指针).数组的基本 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"),请为 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’d 在一起的整数,或者从具有必要要求的数组的 flags 属性返回的标志对象.

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

完整示例#

在此示例中,我们将演示如何使用 ctypes 实现先前使用其他方法实现的加法函数和滤波器函数.首先,实现算法的 C 代码包含函数 zadd , dadd , sadd , cadddfilter2d . 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 , daddsadd 的代码类似,它们分别处理复数浮点型,双精度浮点型和浮点型数据类型:

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 的 shared_library.在 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 带来更多功能,从而消除扩展 Python 和使用 ctypes 分发扩展的困难.

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

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

SWIG#

Simplified Wrapper and Interface Generator (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 编写自定义接口通常很麻烦,因为它必须使用 typemaps 的概念来完成,typemaps 不是 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 风格的连续数组.