使用 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 包含目录当然只有在你正在扩展模块中使用 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)

关于代码,有两点需要注意:首先,不可能将 memory view 返回给 Python.相反,首先创建一个 NumPy 数组 out ,然后使用此数组的视图 b 进行计算.其次,视图 b 的类型为 double[:, ::1] .这意味着具有连续行的二维数组,即 C 矩阵顺序.显式指定顺序可以加速某些算法,因为它们可以跳过步幅计算.

结论#

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

使用 Cython 有几个缺点:

  1. 在编写自定义算法时,有时在包装现有的 C 库时,需要熟悉 C.特别是,在使用 C 内存管理( malloc 和 friends)时,很容易引入内存泄漏.但是,仅仅编译重命名为 .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 例程正在执行任何重要的工作,则此开销应该可以忽略不计.如果你是一位拥有薄弱 C 技能的伟大的 Python 程序员,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 ints/longs,字符串和 unicode 对象会根据需要自动转换为等效的 ctypes 参数.None 对象也会自动转换为 NULL 指针.所有其他 Python 对象必须转换为 ctypes 特定的类型.有两种方法可以绕过此限制,使 ctypes 可以与其他对象集成.

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

  2. 将 argtypes 属性设置为一个列表,该列表的条目包含具有名为 from_param 的 classmethod 的对象,该方法知道如何将您的对象转换为 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,shape和strides可以提供与数组的数据区域,形状和步长相对应的ctypes兼容类型.data属性返回一个 c_void_p ,表示指向数据区域的指针.shape和strides属性各自返回一个ctypes整数数组(如果为0维数组,则返回None,表示NULL指针).数组的基本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”),请使用None作为restype:

func1.restype = None

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

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

值为 None 的关键字参数不进行检查.指定关键字会强制在转换为ctypes兼容对象时检查ndarray的该方面.dtype关键字可以是任何被理解为数据类型对象的对象.ndim关键字应该是一个整数,shape关键字应该是一个整数或一个整数序列.flags关键字指定传递的任何数组上所需的最小标志.这可以指定为以逗号分隔的要求字符串,指示要求位OR’d在一起的整数,或者从具有必要要求的数组的flags属性返回的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 的共享库.在 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 数组,但可以通过使用类型映射使其与 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 风格的连续数组.