Using Python as glue

没有谈话比每个人都更无聊
同意。
- Michel de Montaigne
胶带就像力。它有一个轻的一面,一个黑暗的一面,和
它把宇宙一起。
- Carl Zwanzig

很多人喜欢说Python是一种梦幻般的胶水语言。希望本章能够说服你这是真的。Python首先用于科学的人通常是使用它来粘合在超级计算机上运行的大型应用程序代码的人。不仅在Python中比在shell脚本或Perl中更好的代码,此外,轻松扩展Python的能力使得创建新的类和类型特别适合于解决的问题相对容易。从这些早期贡献者的交互中,Numeric出现了一个类似数组的对象,可以用于在这些应用程序之间传递数据。

随着Numeric已经成熟并发展成NumPy,人们已经能够在NumPy中直接编写更多的代码。通常这个代码对于生产使用是足够快的,但是仍然有时需要访问编译的代码。

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

Calling other compiled libraries from Python

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

它们是调用编译代码的两种基本方法:编写一个扩展模块,然后使用import命令将其导入到Python,或使用ctypes模块直接从Python调用共享库子例程。编写扩展模块是最常用的方法。

警告

从Python调用C代码可能导致Python崩溃,如果你不小心。本章中的方法都是免疫的。你必须知道关于NumPy和正在使用的第三方库处理数据的方式。

Hand-generated wrappers

Writing an extension module中讨论了扩展模块。与编译代码接口的最基本的方法是编写一个扩展模块并构造一个调用编译代码的模块方法。为了提高可读性,您的方法应该利用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接口)。

Creating source for a basic extension module

引入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编译和使用。

Creating a compiled extension module

你也可以得到f2py编译add.f并编译其生成的扩展模块,只留下一个可以从Python导入的共享库扩展文件:

f2py -c -m add add.f

此命令留下一个名为add的文件。{ext}在当前目录(其中{ext}是你的平台上的python扩展模块的适当扩展名 - 如pyd,等。)。然后可以从Python导入此模块。它将包含add(zadd,cadd,dadd,sadd)中每个子程序的方法。每个方法的docstring包含有关如何调用模块方法的信息:

>>> import add
>>> print add.zadd.__doc__
zadd - Function signature:
  zadd(a,b,c,n)
Required arguments:
  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

Improving the basic interface

默认接口是fortran代码到Python的非常直接的翻译。Fortran数组参数现在必须是NumPy数组,整数参数应该是一个整数。该接口将尝试将所有参数转换为它们所需的类型(和形状),并且如果不成功则发出错误。然而,因为它不知道参数的语义(因此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

通过放置intent指令和检查代码,接口可以清理相当多,直到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

意向指令intent(out)用于告诉f2py c是一个输出变量,应该在传递给底层代码之前由接口创建。intent(hide)指令告诉f2py不允许用户指定变量n,而是从a的大小获取它。需要依赖(a)指令来告诉f2py,n的值取决于输入a(因此它不会在创建变量a之前尝试创建变量n)。

修改add.pyf后,可以通过编译add.f95add.pyf来生成新的python模块文件:

f2py -c add.pyf add.f95

新接口有docstring:

>>> import add
>>> print add.zadd.__doc__
zadd - Function signature:
  c = zadd(a,b)
Required arguments:
  a : input rank-1 array('D') with bounds (n)
  b : input rank-1 array('D') with bounds (n)
Return objects:
  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])

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

Inserting directives in Fortran source

漂亮的界面也可以通过将变量指令作为特殊注释放在原始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的结果签名与之前创建的完全相同。If the original source code had contained A(N) instead of A(*) and so forth with B and C, then I could obtain (nearly) the same interface simply by placing the INTENT(OUT) :: C comment line in the source code. 唯一的区别是N将是默认为A长度的可选输入。

A filtering example

用于与待讨论的其他方法比较。这里是使用固定平均滤波器对双精度浮点数的二维数组进行滤波的函数的另一示例。使用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

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

Calling f2py from Python

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关键字将适当的文件名提供给编译函数。

Automatic extension module generation

如果你想分发你的f2py扩展模块,那么你只需要包括.pyf文件和Fortran代码。NumPy中的distutils扩展允许您完全根据此接口文件定义扩展模块。A valid setup.py file allowing distribution of the add.f module (as part of the package f2py_examples so that it would be loaded as f2py_examples.add) is:

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())

新包装的安装很容易使用:

python setup.py install

假设你有适当的权限写入主要的site- packages目录中你所使用的Python版本。要使生成的包工作,您需要创建一个名为__init__.py的文件(在与add.pyf相同的目录中)。注意,扩展模块完全按照add.pyfadd.f文件来定义。将.pyf文件转换为.c文件由numpy.disutils处理。

Conclusion

接口定义文件(.pyf)是你如何微调Python和Fortran之间的接口。有很好的文档,在numpy / f2py / docs目录中找到f2py,其中NumPy安装在您的系统上(通常在site-packages)。还有更多关于使用f2py(包括如何使用它来包装C代码)在http://www.scipy.org/Cookbook在“使用与其他语言的NumPy”标题下的信息。

连接编译代码的f2py方法是目前最复杂和集成的方法。它允许Python与编译代码的干净的分离,同时仍然允许单独分发扩展模块。唯一的缺点是它需要一个Fortran编译器的存在,以便用户安装代码。然而,随着自由编译器g77,gfortran和g95,以及高质量的商业编译器的存在,这种限制不是特别繁重。在我看来,Fortran仍然是编写快速和清晰的科学计算代码的最简单的方法。它以最直接的方式处理复数和多维索引。然而,请注意,一些Fortran编译器不能优化代码以及好的手写C代码。

Cython

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

如果你正在写一个扩展模块,它将包含相当多的自己的算法代码,那么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 include目录,当然,只有当你使用NumPy数组在扩展模块(这是我们假设你使用Cython)。NumPy中的distutils扩展还支持自动生成扩展模块并将其与.pyx文件相链接。它的工作原理,如果用户没有安装Cython,那么它会查找一个文件名相同,但是一个.c扩展名,然后使用,而不是试图产生.c文件。

如果你只是使用Cython来编译一个标准的Python模块,那么你将得到一个C扩展模块,它的运行速度比等效的Python模块快一些。通过使用cdef关键字静态定义C变量,可以获得进一步的速度增加。

让我们看看我们之前看到的两个例子,看看如何使用Cython实现它们。这些例子使用Cython 0.21.1编译成扩展模块。

Complex addition in 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的连续数组是int[::1],而浮点数矩阵是float [:, :] t4 >

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

除了视图语法之外,该函数可以立即被Python程序员读取。变量i的静态类型是隐式的。

Image filter in 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

此2-d平均滤波器运行快速,因为循环在C中,并且指针计算仅在需要时完成。如果上述代码被编译为模块image,则可以使用以下代码非常快速地过滤2-d图像img

import image
out = image.filter(img)

关于代码,有两点值得注意:首先,不可能向Python返回一个内存视图。相反,首先创建NumPy数组out,然后将用于该数组的视图b用于计算。其次,视图b被输入double [:, :: 1]这意味着具有连续行的2-d数组,即C矩阵顺序。明确指定顺序可以加快一些算法,因为它们可以跳过步幅计算。

Conclusion

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

使用Cython有几个缺点:

  1. 当编码自定义算法时,有时当包装现有的C库时,需要熟悉C。特别是,当使用C内存管理(malloc和朋友)时,很容易引入内存泄漏。但是,只是编译重命名为.pyx的Python模块可以加快它,并且添加一些类型声明可以在一些代码中产生显着的加速。
  2. 很容易失去Python和C之间的一个干净的分离,这使得重新使用您的C代码为其他非Python相关项目更加困难。
  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参数从库调用函数。

Having a shared library

对于可以与平台特定的ctypes一起使用的共享库有几个要求。本指南假设您对在系统上制作共享库有些熟悉(或者只有您可以使用共享库)。要记住的项目是:

  • A shared library must be compiled in a special way ( e.g. using the -shared flag with gcc).

  • 在某些平台(例如 Windows)上,共享库需要一个指定要导出的函数的.def文件。例如,mylib.def文件可能包含:

    LIBRARY mylib.dll
    EXPORTS
    cool_function1
    cool_function2
    

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

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

Loading the shared library

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

lib = ctypes.cdll[<full_path_name>]

但是,在访问cdll方法的属性的Windows上,将通过在当前目录或PATH中找到的名称加载第一个DLL。加载绝对路径名需要一个很好的跨平台工作,因为共享库的扩展不同。有一个ctypes.util.find_library实用程序可以简化查找库加载的过程,但它不是万无一失。复杂的事情,不同的平台有不同的共享库使用的默认扩展(例如.dll - Windows,.so - Linux,.dylib - Mac OS X)。如果你使用ctypes来包装需要在几个平台上工作的代码,这也必须考虑。

NumPy提供了一个方便的函数,名为ctypeslib.load_library(name,path)。此函数使用共享库的名称(包括任何前缀,如'lib',但不包括扩展名)和共享库可以位于的路径。如果ctypes模块不可用,则返回一个ctypes库对象或者引发一个OSError(如果找不到该库)或引发一个ImportError(Windows用户:使用load_library加载的ctypes库对象总是加载,假设是cdecl调用约定。有关在其他调用约定下加载库的方法,请参阅ctypes.windll和/或ctypes.oledll下的ctypes文档。

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

Converting arguments

Python ints / longs,strings和unicode对象会根据需要自动转换为等效的ctypes参数。None对象也会自动转换为NULL指针。所有其他Python对象必须转换为ctypes特定的类型。有两个方法围绕这个限制,允许ctypes与其他对象集成。

  1. 不要设置函数对象的argtypes属性,并为要传入的对象定义_as_parameter_方法。_as_parameter_方法必须返回一个Python int,它将被直接传递给函数。
  2. 将argtypes属性设置为一个列表,其条目包含具有名为from_param的类方法的对象,知道如何将对象转换为ctypes可以理解的对象(一个int / long,string,unicode或对象与_as_parameter_

NumPy使用两种方法优先选择第二种方法,因为它可以更安全。ndarray的ctypes属性返回一个具有_as_parameter_属性的对象,该属性返回表示与其相关联的ndarray的地址的整数。因此,可以将此ctypes属性对象直接传递到一个函数,该函数需要一个指向您的ndarray中的数据的指针。调用者必须确保ndarray对象具有正确的类型,形状,并且具有正确的标志集或者如果传递了不适当数组的数据指针,就会导致危险性崩溃。

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

ndarray的ctypes属性也具有额外的属性,当将关于数组的附加信息传递给ctypes函数时,可以很方便。属性数据形状跨度可以提供对应于数据组的数据区域,形状和步幅的ctypes兼容类型。数据属性重写代表指向数据区的指针的c_void_pshape和strides属性每个返回一个ctypes整数的数组(或None表示一个NULL指针,如果为0-d数组)。数组的基本ctype是与平台上的指针相同大小的ctype整数。There are also methods data_as({ctype}), shape_as(<base ctype>), and strides_as(<base ctype>). 这些返回数据作为您选择的ctype对象,shape / strides数组使用您选择的底层基类型。为方便起见,ctypeslib模块还包含c_intp作为ctypes整数数据类型,其大小与平台上的c_void_p大小相同(如果未安装ctypes,则其值为None)。

Calling the function

该函数作为加载的共享库的属性或项目访问。因此,如果./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'd在一起的整数,或从具有必要要求的数组的flags属性返回的flags对象。

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

Complete example

在这个例子中,我将演示如何使用ctypes实现之前使用其他方法实现的加法函数和过滤函数。First, the C code which implements the algorithms contains the functions zadd, dadd, sadd, cadd, and 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++;
    }
}

类似的代码分别处理复杂的float,double和float数据类型的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等价的代码的一个可能的优点是它需要任意strided(即非连续数组),也可以运行更快,取决于你的编译器的优化能力。但是,它显然比filter.f中的简单代码更复杂。此代码必须编译到共享库中。在我的Linux系统上,这是通过使用:

gcc -o code.so -shared code.c

这将在当前目录中创建一个名为code.so的shared_library。在Windows上,不要忘记在每个函数定义前面的行前添加__declspec(dllexport),或者编写一个列出要导出的函数的名称的code.def文件。

应构建此共享库的合适的Python接口。为此,创建一个名为interface.py的文件,在顶部有以下行:

__all__ = ['add', 'filter2d']

import numpy as N
import os

_path = os.path.dirname('__file__')
lib = N.ctypeslib.load_library('code', _path)
_typedict = {'zadd' : complex, 'sadd' : N.single,
             'cadd' : N.csingle, 'dadd' : float}
for name in _typedict.keys():
    val = getattr(lib, name)
    val.restype = None
    _type = _typedict[name]
    val.argtypes = [N.ctypeslib.ndpointer(_type,
                      flags='aligned, contiguous'),
                    N.ctypeslib.ndpointer(_type,
                      flags='aligned, contiguous'),
                    N.ctypeslib.ndpointer(_type,
                      flags='aligned, contiguous,'\
                            'writeable'),
                    N.ctypeslib.c_intp]

此代码加载位于与此文件相同路径中的名为code.{ext}的共享库。然后,向库中包含的函数添加一个void返回类型。它还向库中的函数添加了参数检查,使得ndarrays可以作为前三个参数以及一个整数(足够大以容纳平台上的指针)作为第四个参数传递。

设置过滤功能是类似的,并允许使用ndarray参数作为前两个参数和指向整数(足够大以处理ndarray的步长和形状)的指针作为最后两个参数调用过滤功能。

lib.dfilter2d.restype=None
lib.dfilter2d.argtypes = [N.ctypeslib.ndpointer(float, ndim=2,
                                       flags='aligned'),
                          N.ctypeslib.ndpointer(float, ndim=2,
                                 flags='aligned, contiguous,'\
                                       'writeable'),
                          ctypes.POINTER(N.ctypeslib.c_intp),
                          ctypes.POINTER(N.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 = N.asanyarray(a)
    func, dtype = select(a.dtype)
    a = N.require(a, dtype, requires)
    b = N.require(b, dtype, requires)
    c = N.empty_like(a)
    func(a,b,c,a.size)
    return c

和:

def filter2d(a):
    a = N.require(a, float, ['ALIGNED'])
    b = N.zeros_like(a)
    lib.dfilter2d(a, b, a.ctypes.strides, a.ctypes.shape)
    return b

Conclusion

使用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分发扩展的困难。

Additional tools you may find useful

这些工具已被发现对其他人使用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语法编写的typemaps的概念来完成。因此,我倾向于喜欢其他胶合策略,并且只尝试使用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.Python,因为我不是C ++的大用户,并使用Boost来包装简单的C子程序通常是过度杀死。它的主要目的是使C ++类在Python中可用。所以,如果你有一组需要整合到Python中的C ++类,请考虑学习和使用Boost.Python。

PyFort

PyFort是一个很好的工具,用于将Fortran和Fortran类C代码包装到Python中,并支持Numeric数组。它是由Paul Dubois,一个杰出的计算机科学家和第一个维护者Numeric(现已退休)写的。值得一提的是,有人会更新PyFort来处理NumPy数组,现在支持Fortran或C风格的连续数组。