# 使用Python作为粘合剂

There is no conversation more boring than the one where everybody agrees.

— Michel de Montaigne

Duct tape is like the force. It has a light side, and a dark side, and it holds the universe together.

— Carl Zwanzig

::: danger 警告

:::

## f2py

F2py允许您自动构建一个扩展模块，该模块与Fortran 77/90/95代码中的例程相连。它能够解析Fortran 77/90/95代码并自动为它遇到的子程序生成Python签名，或者你可以通过构造一个接口定义文件（或修改f2py生成的文件）来指导子程序如何与Python接口。 ）。

### 创建基本扩展模块的源

``````C
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 -m add add.f
``````

### 创建编译的扩展模块

``````f2py -c -m add add.f
``````

``````>>> import add
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
``````

### 改善基本界面

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

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

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

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

intent指令，intent（out）用于告诉`c`作为输出变量的f2py，并且应该在传递给底层代码之前由接口创建。intent（hide）指令告诉f2py不允许用户指定变量`n`，而是从大小中获取它`a`。depend（`a`）指令必须告诉f2py n的值取决于输入a（因此在创建变量a之前它不会尝试创建变量n）。

``````f2py -c add.pyf add.f95
``````

``````>>> import add
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])
``````

### 在Fortran源中插入指令

``````C
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
``````

### 过滤示例

``````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
``````

``````f2py -c -m filter filter.f
``````

### 从Python中调用f2py

f2py程序是用Python编写的，可以在代码中运行，以便在运行时编译Fortran代码，如下所示：

``````from numpy import f2py
``````

### 自动扩展模块生成

``````def configuration(parent_package='', top_path=None)
from numpy.distutils.misc_util import Configuration
config = Configuration('f2py_examples',parent_package, top_path)
return config

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

``````pip install .
``````

## 用Cython

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

``````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})
``````

### Cython中的复杂添加

``````cimport cython
cimport numpy as np
import numpy as np

# We need to initialize NumPy.
np.import_array()

#@cython.boundscheck(False)
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
``````

### 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
``````

``````import image
out = image.filter(img)
``````

### Cython结论

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

1. 在编写自定义算法时，有时在包装现有C库时，需要熟悉C语言。特别是，当使用C内存管理（`malloc`和朋友）时，很容易引入内存泄漏。但是，只是编译重命名的Python模块`.pyx` 已经可以加快速度，并且添加一些类型声明可以在某些代码中提供显着的加速。
2. 很容易在Python和C之间失去一个清晰的分离，这使得重用你的C代码用于其他非Python相关项目变得更加困难。
3. Cython生成的C代码难以阅读和修改（并且通常编译有令人烦恼但无害的警告）。

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

## ctypes

ctypes 是一个包含在stdlib中的Python扩展模块，它允许您直接从Python调用共享库中的任意函数。这种方法允许您直接从Python接口C代码。这开辟了大量可供Python使用的库。然而，缺点是编码错误很容易导致丑陋的程序崩溃（就像C中可能发生的那样），因为对参数进行的类型或边界检查很少。当数组数据作为指向原始内存位置的指针传入时尤其如此。那么你应该负​​责子程序不会访问实际数组区域之外的内存。但，

1. 有一个共享的库。
2. 加载共享库。
3. 将python对象转换为ctypes理解的参数。
4. 使用ctypes参数从库中调用函数。

### 加载共享库

• 必须以特殊方式编译共享库（ 例如， 使用`-shared`带有gcc 的标志）。
• 在某些平台（ 例如 Windows）上，共享库需要一个.def文件，该文件指定要导出的函数。例如，mylib.def文件可能包含：
``````LIBRARY mylib.dll
EXPORTS
cool_function1
cool_function2
``````

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

### 加载共享库

``````lib = ctypes.cdll[<full_path_name>]
``````

NumPy提供称为`ctypeslib.load_library`（名称，路径）的便利功能 。此函数采用共享库的名称（包括任何前缀，如'lib'但不包括扩展名）和共享库所在的路径。它返回一个ctypes库对象，或者`OSError`如果找不到库则引发一个或者`ImportError`如果ctypes模块不可用则引发一个。（Windows用户：使用加载的ctypes库对象 `load_library`总是在假定cdecl调用约定的情况下加载。请参阅下面的ctypes文档`ctypes.windll`和/或`ctypes.oledll` 了解在其他调用约定下加载库的方法）。

### 转换参数

Python int / long，字符串和unicode对象会根据需要自动转换为等效的ctypes参数None对象也会自动转换为NULL指针。必须将所有其他Python对象转换为特定于ctypes的类型。围绕此限制有两种方法允许ctypes与其他对象集成。

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

NumPy使用两种方法，优先选择第二种方法，因为它可以更安全。ndarray的ctypes属性返回一个对象，该对象具有一个`_as_parameter_`返回整数的属性，该整数表示与之关联的ndarray的地址。因此，可以将此ctypes属性对象直接传递给期望指向ndarray中数据的指针的函数。调用者必须确保ndarray对象具有正确的类型，形状，并且设置了正确的标志，否则如果传入指向不适当数组的数据指针则会导致令人讨厌的崩溃。

ndarray的ctypes属性还赋予了额外的属性，这些属性在将有关数组的其他信息传递给ctypes函数时可能很方便。属性数据形状步幅可以提供与数据区域，形状和数组步幅相对应的ctypes兼容类型。data属性返回`c_void_p`表示指向数据区域的指针。shape和strides属性各自返回一个ctypes整数数组（如果是0-d数组，则返回None表示NULL指针）。数组的基本ctype是与平台上的指针大小相同的ctype整数。还有一些方法 `data_as({ctype})`，和`shape_as()`strides_as()`。它们将数据作为您选择的ctype对象返回，并使用您选择的基础类型返回shape / strides数组。为方便起见，该`ctypeslib`模块还包含`c_intp`一个ctypes整数数据类型，其大小`c_void_p``与平台上的大小相同 （如果未安装ctypes，则其值为None）。

### 调用函数

``````lib = numpy.ctypeslib.load_library('mylib','.')
func1 = lib.cool_function1  # or equivalently
func1 = lib['cool_function1']
``````

``````func1.restype = None
``````

`ndpointer`dtype = Nonendim = Noneshape = Noneflags = None

`None`不检查具有该值的关键字参数。指定关键字会强制在转换为与ctypes兼容的对象时检查ndarray的该方面。dtype关键字可以是任何被理解为数据类型对象的对象。ndim关键字应为整数，shape关键字应为整数或整数序列。flags关键字指定传入的任何数组所需的最小标志。这可以指定为逗号分隔要求的字符串，指示需求位OR'd在一起的整数，或者从flags的flags属性返回的flags对象。具有必要要求的数组。

### 完整的例子

``````/* 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++;
}
}
``````

``````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;
}
}
}
``````

``````gcc -o code.so -shared code.c
``````

``````__all__ = ['add', 'filter2d']

import numpy as np
import os

_path = os.path.dirname('__file__')
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]
``````

``````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']:
elif dtype.char in ['F']:
elif dtype.char in ['DG']:
else:
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之外，无需学习新的语法
• 允许重复使用C代码
• 可以使用简单的Python包装器获取为其他目的编写的共享库中的功能并搜索库。
• 通过ctypes属性轻松与NumPy集成
• 使用ndpointer类工厂进行完整的参数检查

• 由于缺乏在distutils中构建共享库的支持，很难分发使用ctypes创建的扩展模块（但我怀疑这会随着时间的推移而改变）。
• 您必须拥有代码的共享库（没有静态库）。
• 很少支持C ++代码及其不同的库调用约定。你可能需要一个围绕C ++代码的C包装器来与ctypes一起使用（或者只是使用Boost.Python）。

## 您可能会觉得有用的其他工具

### SIP

SIP是另一种用于包装特定于Python的C / C ++库的工具，似乎对C ++有很好的支持。Riverbank Computing开发了SIP，以便为QT库创建Python绑定。必须编写接口文件以生成绑定，但接口文件看起来很像C / C ++头文件。虽然SIP不是一个完整的C ++解析器，但它理解了相当多的C ++语法以及它自己的特殊指令，这些指令允许修改Python绑定的完成方式。它还允许用户定义Python类型和C / C ++结构和类之间的映射。

### 提升Python

Boost是C ++库的存储库，Boost.Python是其中一个库，它提供了一个简洁的接口，用于将C ++类和函数绑定到Python。Boost.Python方法的神奇之处在于它完全在纯C ++中工作而不引入新语法。许多C ++用户报告称，Boost.Python可以无缝地结合两者的优点。我没有使用过Boost.Python，因为我不是C ++的大用户，并且使用Boost来包装简单的C子例程通常都是过度杀戮。它的主要目的是使Python中的C ++类可用。因此，如果您有一组需要完全集成到Python中的C ++类，请考虑学习并使用Boost.Python。

### PyFort

PyFort是一个很好的工具，可以将Fortran和类似Fortran的C代码包装到Python中，并支持数值数组。它由着名计算机科学家Paul Dubois编写，是Numeric（现已退休）的第一个维护者。值得一提的是希望有人会更新PyFort以使用NumPy数组，现在支持Fortran或C风格的连续数组。