使用python作为胶水

没有比每个人都无聊的谈话了
同意。
--- 蒙塔尼米歇尔
管道胶带就像一股力量。它有光明的一面和黑暗的一面,以及
它把宇宙联系在一起。
--- 卡尔茨瓦齐格

许多人喜欢说,Python是一种奇妙的粘合语言。希望这一章能让你相信这是真的。Python在科学上的第一个采用者通常是那些使用它来粘合在超级计算机上运行的大型应用程序代码的人。不仅用python编写代码比用shell脚本或perl编写代码要好得多,而且,轻松扩展python的能力使创建新的类和类型变得相对容易,这些类和类型专门适合要解决的问题。从这些早期贡献者的交互中,numeric出现了一个类似数组的对象,可以用来在这些应用程序之间传递数据。

随着numeric的成熟和发展,人们可以直接用numpy编写更多的代码。通常,此代码对于生产使用来说足够快,但有时仍然需要访问已编译的代码。要么从算法中获得最后一点效率,要么更容易访问用C/C++或FORTRAN编写的广泛可用代码。

本章将回顾许多可用于访问用其他编译语言编写的代码的工具。有许多资源可供学习从Python调用其他编译库,本章的目的不是让您成为专家。主要目标是让你意识到一些可能性,这样你就会知道“谷歌”是什么,以便了解更多。

从python调用其他已编译的库

虽然python是一种伟大的语言,而且很乐意编写代码,但它的动态特性会导致开销,从而导致一些代码( i.e. for循环内部的原始计算)比用静态编译语言编写的等效代码慢10-100倍。此外,由于临时数组是在计算过程中创建和销毁的,因此它可能会导致内存使用量比需要的要大。对于许多类型的计算需求,额外的减速和内存消耗通常是无法避免的(至少对于时间或内存关键的代码部分)。因此,最常见的需求之一是从Python代码中调用一个快速的机器代码例程(例如使用C/C++或Fortran编译)。事实上,这相对容易做到,这就是为什么Python是科学和工程编程的优秀高级语言的一个重要原因。

它们是调用编译代码的两种基本方法:编写一个扩展模块,然后使用import命令导入到python,或者使用 ctypes 模块。编写扩展模块是最常见的方法。

警告

如果不小心,从python调用C代码可能会导致python崩溃。本章中的所有方法都不是免疫的。您必须了解numpy和正在使用的第三方库处理数据的方式。

手工制作的包装纸

扩展模块在 编写扩展模块 . 与已编译代码接口的最基本方法是编写扩展模块并构造调用已编译代码的模块方法。为了提高可读性,您的方法应该利用 PyArg_ParseTuple 调用在Python对象和C数据类型之间转换。对于标准的C数据类型,可能已经有了一个内置的转换器。对于其他人,您可能需要编写自己的转换器并使用 "O&" 格式字符串,它允许您指定一个函数,该函数将用于执行从Python对象到所需的任何C结构的转换。

一旦转换到适当的C结构和C数据类型,包装器的下一步就是调用底层函数。如果底层函数是C或C++,这是很简单的。但是,为了调用FORTRAN代码,您必须熟悉如何使用编译器和平台从C/C++中调用FORTRAN子程序。这在一定程度上会改变平台和编译器(这也是f2py使与Fortran代码的接口更加简单的另一个原因),但通常涉及到名称的下划线管理以及所有变量都是通过引用传递的事实(即所有参数都是指针)。

手工生成的包装器的优点是,您可以完全控制C库的使用和调用方式,这将导致一个简洁、紧密的接口,并且具有最小的over head。缺点是您必须编写、调试和维护C代码,尽管大部分代码都可以使用其他扩展模块中的“剪切粘贴和修改”这一久负盛名的技术进行调整。因为调用附加C代码的过程相当有条理,所以开发了代码生成过程以使此过程更简单。其中一种代码生成技术是用numpy发布的,允许与Fortran和(简单的)C代码轻松集成。本文件包(F2PY)将在下一节中简要介绍。

F2PY

f2py允许您自动构造一个扩展模块,该模块与Fortran 77/90/95代码中的例程接口。它能够解析Fortran 77/90/95代码并自动为遇到的子例程生成python签名,或者您可以通过构造接口定义文件(或修改f2py生成的文件)来指导子例程如何与python交互。

为基本扩展模块创建源

介绍f2py最简单的方法可能是提供一个简单的例子。以下是名为 add.f

C
      SUBROUTINE ZADD(A,B,C,N)
C
      DOUBLE COMPLEX A(*)
      DOUBLE COMPLEX B(*)
      DOUBLE COMPLEX C(*)
      INTEGER N
      DO 20 J = 1, N
         C(J) = A(J)+B(J)
 20   CONTINUE
      END

这个例程简单地将元素添加到两个连续的数组中,并将结果放入第三个数组中。调用例程必须提供所有三个数组的内存。这个程序的一个非常基本的接口可以通过f2py自动生成:

f2py -m add add.f

如果搜索路径设置正确,则应该能够运行此命令。此命令将在当前目录中生成名为addmodule.c的扩展模块。现在可以从python编译和使用这个扩展模块,就像其他扩展模块一样。

创建已编译扩展模块

您还可以让f2py编译add.f并编译其生成的扩展模块,只留下一个可以从python导入的共享库扩展文件:

f2py -c -m add add.f

This command leaves a file named add.{ext} in the current directory (where {ext} is the appropriate extension for a Python extension module on your platform --- so, pyd, etc. ). This module may then be imported from Python. It will contain a method for each subroutine in add (zadd, cadd, dadd, sadd). The docstring of each method contains information about how the module method may be called:

>>> import add
>>> print(add.zadd.__doc__)
zadd(a,b,c,n)

Wrapper for ``zadd``.

Parameters
----------
a : input rank-1 array('D') with bounds (*)
b : input rank-1 array('D') with bounds (*)
c : input rank-1 array('D') with bounds (*)
n : input int

改进基本接口

默认接口是将Fortran代码直接翻译成Python。Fortran数组参数现在必须是NumPy数组,integer参数应该是整数。接口将尝试将所有参数转换为它们所需的类型(和形状),如果不成功,则发出错误。但是,由于它对参数的语义一无所知(例如C是一个输出,而n应该真正匹配数组大小),因此有可能以可能导致Python崩溃的方式滥用此函数。例如::

>>> add.zadd([1, 2, 3], [1, 2], [3, 4], 1000)

会导致大多数系统的程序崩溃。在封面下,列表被转换成适当的数组,但是底层的添加循环被告知要循环到已分配内存的边界之外。

为了改进接口,应该提供指令。这是通过构造接口定义文件来实现的。通常最好从f2py可以生成的接口文件开始(从中获取默认行为)。要让f2py生成接口文件,请使用-h选项:

f2py -h add.pyf -m add add.f

此命令将文件add.pyf保留在当前目录中。与zadd对应的文件部分是:

subroutine zadd(a,b,c,n) ! in :add:add.f
   double complex dimension(*) :: a
   double complex dimension(*) :: b
   double complex dimension(*) :: c
   integer :: n
end subroutine zadd

通过放置意向指令和检查代码,可以对接口进行相当大的清理,直到Python模块方法既易于使用又更健壮为止。

subroutine zadd(a,b,c,n) ! in :add:add.f
   double complex dimension(n) :: a
   double complex dimension(n) :: b
   double complex intent(out),dimension(n) :: c
   integer intent(hide),depend(a) :: n=len(a)
end subroutine zadd

意向指令,意向(out)用于告诉f2py c 是一个输出变量,在传递给基础代码之前应由接口创建。intent(hide)指令告诉f2py不要允许用户指定变量, n 而是从 a . 依赖性(依赖性) a )指令有必要告诉f2py n的值取决于输入a(以便它在创建变量a之前不会尝试创建变量n)。

修改后 add.pyf ,新的Python模块文件可以通过编译 add.fadd.pyf ::

f2py -c add.pyf add.f

新接口具有docstring::

>>> import add
>>> print(add.zadd.__doc__)
c = zadd(a,b)

Wrapper for ``zadd``.

Parameters
----------
a : input rank-1 array('D') with bounds (n)
b : input rank-1 array('D') with bounds (n)

Returns
-------
c : rank-1 array('D') with bounds (n)

现在,可以用更健壮的方式调用函数:

>>> add.zadd([1, 2, 3], [4, 5, 6])
array([5.+0.j, 7.+0.j, 9.+0.j])

请注意自动转换为正确的格式。

在Fortran源中插入指令

nice接口也可以通过在原始Fortran代码中将变量指令作为特殊注释来自动生成。因此,如果我修改源代码以包含:

C
      SUBROUTINE ZADD(A,B,C,N)
C
CF2PY INTENT(OUT) :: C
CF2PY INTENT(HIDE) :: N
CF2PY DOUBLE COMPLEX :: A(N)
CF2PY DOUBLE COMPLEX :: B(N)
CF2PY DOUBLE COMPLEX :: C(N)
      DOUBLE COMPLEX A(*)
      DOUBLE COMPLEX B(*)
      DOUBLE COMPLEX C(*)
      INTEGER N
      DO 20 J = 1, N
         C(J) = A(J) + B(J)
 20   CONTINUE
      END

然后,我可以使用以下方法编译扩展模块:

f2py -c -m add add.f

函数add.zadd的结果签名与先前创建的签名完全相同。如果原始源代码包含 A(N) 而不是 A(*) 等等 BC ,然后我可以通过放置 INTENT(OUT) :: C 源代码中的注释行。唯一的区别是 N 将是默认长度为 A .

过滤示例

与其他方法进行比较。下面是一个函数的另一个例子,该函数使用固定平均过滤器过滤二维双精度浮点数数组。使用fortran索引多维数组的优点应该从这个例子中看出来。

      SUBROUTINE DFILTER2D(A,B,M,N)
C
      DOUBLE PRECISION A(M,N)
      DOUBLE PRECISION B(M,N)
      INTEGER N, M
CF2PY INTENT(OUT) :: B
CF2PY INTENT(HIDE) :: N
CF2PY INTENT(HIDE) :: M
      DO 20 I = 2,M-1
         DO 40 J=2,N-1
            B(I,J) = A(I,J) +
     $           (A(I-1,J)+A(I+1,J) +
     $            A(I,J-1)+A(I,J+1) )*0.5D0 +
     $           (A(I-1,J-1) + A(I-1,J+1) +
     $            A(I+1,J-1) + A(I+1,J+1))*0.25D0
 40      CONTINUE
 20   CONTINUE
      END

可以使用以下方法编译此代码并将其链接到名为filter的扩展模块中:

f2py -c -m filter filter.f

这将在当前目录中使用名为dfilter2d的方法生成名为filter.so的扩展模块,该方法返回已筛选的输入版本。

从python调用f2py

f2py程序是用python编写的,可以从代码内部运行,以便在运行时编译Fortran代码,如下所示:

from numpy import f2py
with open("add.f") as sourcefile:
    sourcecode = sourcefile.read()
f2py.compile(sourcecode, modulename='add')
import add

源字符串可以是任何有效的Fortran代码。如果要保存扩展模块源代码,则可以通过 source_fn 编译函数的关键字。

自动扩展模块生成

如果您想分发您的f2py扩展模块,那么您只需要包含.pyf文件和Fortran代码。numpy中的distutils扩展允许您完全根据这个接口文件定义扩展模块。有效的 setup.py 允许分发的文件 add.f 模块(作为包的一部分 f2py_examples 以便将其加载为 f2py_examples.add 是:

def configuration(parent_package='', top_path=None)
    from numpy.distutils.misc_util import Configuration
    config = Configuration('f2py_examples',parent_package, top_path)
    config.add_extension('add', sources=['add.pyf','add.f'])
    return config

if __name__ == '__main__':
    from numpy.distutils.core import setup
    setup(**configuration(top_path='').todict())

安装新软件包很容易使用:

pip install .

假设您有适当的权限为所使用的Python版本写入主站点包目录。要使生成的包工作,需要创建一个名为 __init__.py (在同一目录中 add.pyf )注意,扩展模块完全是根据 add.pyfadd.f 文件夹。.pyf文件到.c文件的转换由 numpy.disutils .

结论

接口定义文件(.pyf)是如何微调Python和Fortran之间的接口的。在numpy/f2py/docs目录中有关于f2py的很好的文档,在您的系统中安装了numpy(通常在站点包下)。在https://scipy-cookbook.readthedocs.io的“Interfaceing with other languages”标题下,还有更多关于使用f2py(包括如何使用它来包装C代码)的信息。

链接编译代码的f2py方法是目前最复杂和最完整的方法。它允许用已编译的代码清晰地分离Python,同时还允许单独分发扩展模块。唯一的收回是,它需要存在一个Fortran编译器,以便用户安装代码。然而,随着免费编译器G77、Gfortran和G95的存在,以及高质量的商业编译器的存在,这种限制并不特别繁重。在我看来,fortran仍然是为科学计算编写快速清晰的代码的最简单方法。它以最简单的方式处理复杂的数字和多维索引。但是,请注意,一些Fortran编译器将无法优化代码以及良好的手写C代码。

赛隆

Cython 是一个Python方言的编译器,它为速度添加(可选)静态类型,并允许将C或C++代码混合到模块中。它生成C或C++扩展,可以在Python代码中编译和导入。

如果您正在编写一个扩展模块,该模块还将包含相当多您自己的算法代码,那么Cython是一个很好的匹配。其特点之一是能够轻松快速地使用多维数组。

注意,Cython只是一个扩展模块生成器。与f2py不同,它不包括编译和链接扩展模块的自动工具(必须以通常的方式完成)。它确实提供了一个名为 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 include目录只有在扩展模块中使用numpy数组时才有必要(我们假设您使用的是cython)。numpy中的distuils扩展还支持自动生成扩展模块并将其与 .pyx 文件。它的工作原理是,如果用户没有安装Cython,那么它会查找文件名相同但 .c 然后使用的扩展,而不是尝试生成 .c 再次归档。

如果您只是使用cython来编译标准的python模块,那么您将得到一个C扩展模块,它通常比等效的python模块运行得更快。使用 cdef 用于静态定义C变量的关键字。

让我们来看两个我们以前见过的例子,看看如何使用Cython实现它们。这些示例使用cython 0.21.1编译成扩展模块。

赛通中的复合加成

这是一个叫赛通模块的一部分 add.pyx 它实现了我们以前使用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 从中加载定义的语句 numpy.pxd 和赛通一起运送的头。看起来numpy被导入了两次; cimport 仅使numpy c-api可用,而常规 import 导致在运行时导入python样式,并使调用熟悉的numpy python API成为可能。

该示例还演示了Cython的“类型化内存视图”,它类似于C级别的numpy数组,从某种意义上说,它们是了解自身范围的形状和跨步数组(与通过裸指针寻址的C数组不同)。句法 double complex[:] 表示具有任意跨距的一维双精度数组(向量)。一个连续的整数数组将是 int[::1] ,而浮点数矩阵将是 float[:, :] .

显示的注释是 cython.boundscheck decorator,它根据每个函数打开或关闭内存视图访问的边界检查。我们可以使用它来进一步加快代码的速度,以牺牲安全性(或者在进入循环之前进行手动检查)。

除了视图语法之外,该函数对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。语言和编译器维护得很好。

使用赛通有几个缺点:

  1. 当对自定义算法进行编码时,有时在包装现有的C库时,需要对C有一些熟悉。尤其是在使用C内存管理时 (malloc 和朋友),很容易引入内存泄漏。但是,只需编译重命名为的python模块 .pyx 已经可以加快速度,添加一些类型声明可以在某些代码中显著加快速度。

  2. 很容易丢失python和c之间的清晰分离,这使得为其他与python无关的项目重新使用C代码变得更加困难。

  3. 由Cython生成的C代码很难读取和修改(通常编译时带有恼人但无害的警告)。

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

C型

Ctypes 是包含在stdlib中的python扩展模块,它允许您直接从python调用共享库中的任意函数。这种方法允许您直接从Python与C代码进行接口。这为从Python中使用打开了大量的库。但是,缺点是编码错误很容易导致丑陋的程序崩溃(就像在C中一样),因为对参数进行的类型或边界检查很少。当数组数据作为指向原始内存位置的指针传入时,这种情况尤其明显。这样子例程就不能访问实际数组区域之外的内存了。但是,如果你不介意生活在一个有点危险的类型可以是一个有效的工具,快速利用一个大的共享库(或编写扩展功能在自己的共享库)。

因为ctypes方法向编译的代码公开了一个原始接口,所以它并不总是能够容忍用户错误。CTypes模块的可靠使用通常需要一个额外的python代码层,以便检查传递给底层子程序的对象的数据类型和数组边界。这个额外的检查层(更不用说从CTypes对象到CTypes本身执行的C数据类型的转换)将使接口比手写扩展模块接口慢。但是,如果被调用的C例程正在做大量的工作,那么这个开销应该可以忽略。如果您是一个优秀的Python程序员,而且C技能很差,那么CTypes是一种向编译代码的(共享)库编写有用接口的简单方法。

要使用ctypes,必须

  1. 有一个共享的库。

  2. 加载共享库。

  3. 将Python对象转换为ctypes参数。

  4. 使用ctypes参数从库中调用函数。

拥有共享的库

共享库有几个要求,可以与特定于平台的CTypes一起使用。本指南假定您对在系统上创建共享库有一定的了解(或者您只需拥有一个可用的共享库)。要记住的项目有:

  • 共享库必须以特殊方式编译( e.g. 使用 -shared GCC标志。

  • 在某些平台上( e.g. 共享库需要一个.def文件来指定要导出的函数。例如a我的定义文件可能包含:

    LIBRARY mylib.dll
    EXPORTS
    cool_function1
    cool_function2
    

    或者,您可以使用存储类说明符 __declspec(dllexport) 在函数的C-定义中避免 .def 文件。

在python distuils中,没有标准的方法以跨平台的方式创建标准共享库(扩展模块是python理解的“特殊”共享库)。因此,在编写本书时,ctypes的一个很大的缺点是很难以跨平台的方式分发使用ctypes并包含您自己的代码的python扩展,这些代码应该编译为用户系统上的共享库。

加载共享库

加载共享库的一种简单而健壮的方法是获取绝对路径名,并使用ctypes的cdll对象加载它:

lib = ctypes.cdll[<full_path_name>]

但是,在访问 cdll 方法将使用在当前目录或路径上找到的名称加载第一个dll。加载绝对路径名需要一些跨平台工作的技巧,因为共享库的扩展有所不同。有一个 ctypes.util.find_library 实用程序,可以简化查找要加载的库的过程,但它不是万无一失的。更复杂的是,不同的平台有不同的默认扩展名,由共享库使用(例如.dll--windows、.so--linux、.dylib--mac os x)。如果您使用CTypes包装需要在多个平台上工作的代码,那么也必须考虑到这一点。

numpy提供了一个名为 ctypeslib.load_library (姓名,路径)。此函数使用共享库的名称(包括任何前缀,如“lib”,但不包括扩展名)和共享库所在的路径。它返回CTypes库对象或引发 OSError 如果找不到库或引发 ImportError 如果CTypes模块不可用。(Windows用户:使用 load_library 总是在假定CDECL调用约定的情况下加载。请参阅下面的ctypes文档 ctypes.windll 和/或 ctypes.oledll 用于在其他调用约定下加载库的方法)。

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

转换参数

python ints/long、strings和unicode对象会根据需要自动转换为等效的ctypes参数。none对象也会自动转换为空指针。所有其他python对象都必须转换为特定于ctypes的类型。围绕这一限制,有两种方法可以使CTypes与其他对象集成。

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

  2. 将argtypes属性设置为一个列表,该列表的条目包含的对象的classmethod名为from_param,它知道如何将对象转换为ctypes可以理解的对象(int/long、string、unicode或具有 _as_parameter_ 属性)。

numpy使用这两种方法,并优先使用第二种方法,因为这样更安全。ndarray的ctypes属性返回一个具有 _as_parameter_ 返回一个整数的属性,该整数表示与其关联的ndarray的地址。因此,可以将此ctypes属性对象直接传递给期望指向ndarray中数据的指针的函数。调用方必须确保Ndarray对象的类型、形状正确,并且设置了正确的标志,如果传入了指向不正确数组的数据指针,则可能导致严重崩溃。

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

ndarray的ctypes属性也被赋予了额外的属性,在将关于数组的额外信息传递到ctypes函数时,这些属性可能很方便。属性 data形状大步 可以提供与数据区域、形状和数组步幅相对应的CTypes兼容类型。data属性返回 c_void_p 表示指向数据区域的指针。shape和steps属性每个都返回一个ctypes整数数组(如果是0-d数组,则无表示空指针)。数组的基ctype是一个与平台上的指针大小相同的ctype整数。还有一些方法 data_as({{ctype}})shape_as(<base ctype>)strides_as(<base ctype>) . 它们将数据作为您选择的ctype对象返回,并使用您选择的基础类型返回shape/steps数组。为了方便起见, ctypeslib 模块还包含 c_intp 作为一个ctypes integer数据类型,其大小与 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关键字指定传入的任何数组所需的最小标志。这可以指定为一个逗号分隔的需求字符串、一个指示需求位或需求位在一起的整数,或者从具有必要需求的数组的flags属性返回的flags对象。

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

完整的例子

在本例中,我将演示如何使用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 分别处理复杂的float、double和float数据类型:

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) 在每个函数定义前一行的void前面,或编写 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包装器来与cType一起使用(或者只使用Boosix.Python)。

由于使用ctypes制作的扩展模块难以分发,因此f2py和cython仍然是扩展python以创建包的最简单方法。然而,在某些情况下,ctypes是一个有用的选择。这将为ctypes带来更多的特性,从而消除扩展python和使用ctypes分发扩展的困难。

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

这些工具已经被其他使用python的人发现很有用,所以这里也包括这些工具。它们是分开讨论的,因为它们要么是现在由f2py、cython或ctypes(swig、pyfort)处理的旧方法,要么是因为我对它们不太了解(sip、boost)。我没有添加这些方法的链接,因为我的经验是,你可以使用谷歌或其他搜索引擎更快地找到最相关的链接,这里提供的任何链接都会很快过时。不要仅仅因为它包含在这个列表中,就认为这个包不值得你注意。我将这些包的信息包括在内,因为许多人发现它们很有用,我想为您提供尽可能多的选项来解决轻松集成代码的问题。

SWIG

简化的包装器和接口生成器(SWIG)是一种旧的和相当稳定的方法来封装C/C++库到各种各样的其他语言。它不专门理解numpy数组,但可以通过使用类型映射使其与numpy一起使用。numpy.i下的numpy/tools/swig目录中有一些示例类型映射,以及使用它们的示例模块。SWIG擅长封装大型C/C++库,因为它可以(几乎)解析它们的头并自动生成接口。从技术上讲,您需要生成 .i 定义接口的文件。然而,通常情况下 .i 文件可以是头本身的一部分。界面通常需要一些调整才能非常有用。这种解析C/C++头文件和自动生成接口的能力仍然使SWIG成为一种从C/C++到Python中添加泛函的有用方法,尽管已经出现了更多针对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++结构和类之间的映射。

增强Python

Boost是C++库的一个库,Boosi.python是其中的一个库,它提供了一个简洁的接口,用于将C++类和函数绑定到Python。Python方法的惊人之处在于它完全在纯C++中工作,而不引入新的语法。许多C++用户报告了Booost。Python使得可以以无缝的方式组合两个世界中最好的两个。我还没有使用Booost。Python,因为我不是C++的大用户,使用Boost来包装简单的C子程序通常是致命的。它的主要目的是使Python中的C++类可用。因此,如果您有一组C++类需要集成到Python中,请考虑学习和使用BooSt.python。

PyFort

Pyfort是一个很好的工具,可以将类似于Fortran和Fortran的C代码打包到Python中,并支持数值数组。这本书是由著名的计算机科学家保罗·杜布瓦(PaulDubois)撰写的,他是第一位数字维护者(现已退休)。值得一提的是,希望有人会更新pyfort,使其也能与numpy数组一起使用,而numpy数组现在支持fortran或c样式的连续数组。