三种包装方法 - 入门#
使用 F2PY 将 Fortran 或 C 函数包装到 Python 包含以下步骤
创建所谓的 签名文件,其中包含对 Fortran 或 C 函数的包装程序的描述,也称为函数的签名。对于 Fortran 例程,F2PY 可以通过扫描 Fortran 源代码并跟踪创建包装函数所需的所有相关信息来创建初始签名文件。
可选地,可以编辑 F2PY 生成的签名文件以优化包装函数,这可以使它们更“智能”和更“Pythonic”。
F2PY 读取签名文件并写入包含 Fortran/C/Python 绑定的 Python C/API 模块。
F2PY 编译所有源代码并构建包含包装程序的扩展模块。
在构建扩展模块时,F2PY 使用
meson
,以前使用numpy.distutils
。有关不同构建系统的详细信息,请参阅 F2PY 和构建系统。
注意
请参阅 1 迁移至 meson 以获取迁移信息。
根据您的操作系统,您可能需要单独安装 Python 开发头文件(提供
Python.h
文件)。在 Linux Debian-based 发行版中,此软件包应称为python3-dev
,在 Fedora-based 发行版中为python3-devel
。对于 macOS,根据 Python 的安装方式,情况可能会有所不同。在 Windows 中,头文件通常已安装,请参阅 F2PY 和 Windows。
注意
F2PY 支持 SciPy 测试的所有操作系统,因此它们的 系统依赖项面板 是一个很好的参考。
根据具体情况,这些步骤可以在单个复合命令中执行,也可以一步一步地执行;在这种情况下,可以省略或与其他步骤组合某些步骤。
下面,我们将介绍三种使用 F2PY 与 Fortran 77 的典型方法。可以按照努力程度递增的顺序阅读这些方法,但它们也针对不同的访问级别,具体取决于 Fortran 代码是否可以自由修改。
以下 Fortran 77 代码将用于说明,将其保存为 fib1.f
C FILE: FIB1.F
SUBROUTINE FIB(A,N)
C
C CALCULATE FIRST N FIBONACCI NUMBERS
C
INTEGER N
REAL*8 A(N)
DO I=1,N
IF (I.EQ.1) THEN
A(I) = 0.0D0
ELSEIF (I.EQ.2) THEN
A(I) = 1.0D0
ELSE
A(I) = A(I-1) + A(I-2)
ENDIF
ENDDO
END
C END FILE FIB1.F
注意
F2PY 解析 Fortran/C 签名以构建用于 Python 的包装函数。但是,它不是编译器,它不会检查源代码中的其他错误,也不会实现整个语言标准。一些错误可能会静默通过(或作为警告),需要用户验证。
快速方法#
将 Fortran 子例程 FIB
包装到 Python 中以供使用的最快方法是运行
python -m numpy.f2py -c fib1.f -m fib1
或者,如果 f2py
命令行工具可用,则可以使用
f2py -c fib1.f -m fib1
注意
因为 f2py
命令可能并非在所有系统中都可用,尤其是在 Windows 上,因此在本指南中,我们将使用 python -m numpy.f2py
命令。
此命令会编译和包装 fib1.f
(-c
) 以在当前目录中创建扩展模块 fib1.so
(-m
)。可以通过执行 python -m numpy.f2py
查看命令行选项列表。现在,在 Python 中,Fortran 子例程 FIB
可以通过 fib1.fib
访问。
>>> import numpy as np
>>> import fib1
>>> print(fib1.fib.__doc__)
fib(a,[n])
Wrapper for ``fib``.
Parameters
----------
a : input rank-1 array('d') with bounds (n)
Other parameters
----------------
n : input int, optional
Default: len(a)
>>> a = np.zeros(8, 'd')
>>> fib1.fib(a)
>>> print(a)
[ 0. 1. 1. 2. 3. 5. 8. 13.]
注意
请注意,F2PY 识别出第二个参数
n
是第一个数组参数a
的维度。由于默认情况下所有参数都是输入参数,因此 F2PY 认为n
可以是可选的,默认值为len(a)
。可以使用可选
n
的不同值>>> a1 = np.zeros(8, 'd') >>> fib1.fib(a1, 6) >>> print(a1) [ 0. 1. 1. 2. 3. 5. 0. 0.]
但当它与输入数组
a
不兼容时,会引发异常>>> fib1.fib(a, 10) Traceback (most recent call last): File "<stdin>", line 1, in <module> fib.error: (len(a)>=n) failed for 1st keyword n: fib:n=10 >>>
F2PY 实现相关参数之间的基本兼容性检查,以避免意外崩溃。
当使用一个 Fortran 连续的 且具有与假定 Fortran 类型相对应的
dtype
的 NumPy 数组作为输入数组参数时,其 C 指针将直接传递给 Fortran。否则,F2PY 会对输入数组进行连续复制(使用正确的
dtype
),并将副本的 C 指针传递给 Fortran 子例程。因此,对输入数组(副本)的任何可能更改都不会影响原始参数,如下所示>>> a = np.ones(8, 'i') >>> fib1.fib(a) >>> print(a) [1 1 1 1 1 1 1 1]
显然,这是意料之外的,因为 Fortran 通常通过引用传递。上述示例使用
dtype=float
能够正常运行被认为是偶然的。F2PY 提供了
intent(inplace)
属性,它会修改输入数组的属性,以便 Fortran 例程进行的任何更改都会反映在输入参数中。例如,如果指定了intent(inplace) a
指令(有关详细信息,请参阅 属性),那么上面的示例将变为>>> a = np.ones(8, 'i') >>> fib1.fib(a) >>> print(a) [ 0. 1. 1. 2. 3. 5. 8. 13.]
但是,让 Fortran 子例程进行的更改传播到 Python 的推荐方法是使用
intent(out)
属性。这种方法更有效,也更清晰。在 Python 中使用
fib1.fib
与在 Fortran 中使用FIB
非常相似。但是,在 Python 中使用 *就地* 输出参数是一种不好的风格,因为 Python 中没有安全机制来防止错误的参数类型。在使用 Fortran 或 C 时,编译器在编译过程中会发现任何类型不匹配,但在 Python 中,必须在运行时检查类型。因此,在 Python 中使用 *就地* 输出参数可能会导致难以发现的错误,更不用说代码在实现所有必要的类型检查时可读性更差。
尽管到目前为止讨论的包装 Fortran 例程以供 Python 使用的方法非常简单,但它有几个缺点(参见上面的评论)。缺点是由于 F2PY 无法确定参数的实际意图;也就是说,区分输入参数和输出参数存在歧义。因此,F2PY 默认情况下假设所有参数都是输入参数。
有一些方法(见下文)可以“教导”F2PY 关于函数参数的真实意图,从而消除这种歧义,F2PY 能够为 Fortran 函数生成更明确、更易于使用且不易出错的包装程序。
智能方法#
如果我们想要对 F2PY 如何处理 Fortran 代码的接口有更多控制,我们可以逐一应用包装步骤。
首先,我们通过运行以下命令从
fib1.f
创建一个签名文件python -m numpy.f2py fib1.f -m fib2 -h fib1.pyf
签名文件保存到
fib1.pyf
(见-h
标志),其内容如下所示。! -*- f90 -*- python module fib2 ! in interface ! in :fib2 subroutine fib(a,n) ! in :fib2:fib1.f real*8 dimension(n) :: a integer optional,check(len(a)>=n),depend(a) :: n=len(a) end subroutine fib end interface end python module fib2 ! This file was auto-generated with f2py (version:2.28.198-1366). ! See http://cens.ioc.ee/projects/f2py2e/
接下来,我们将教 F2PY 参数
n
是一个输入参数(使用intent(in)
属性),并且结果,即调用 Fortran 函数FIB
后a
的内容,应该返回到 Python(使用intent(out)
属性)。此外,应该使用输入参数n
确定的尺寸动态创建数组a
(使用depend(n)
属性来指示这种依赖关系)。经适当修改的
fib1.pyf
版本(保存为fib2.pyf
)的内容如下! -*- f90 -*- python module fib2 interface subroutine fib(a,n) real*8 dimension(n),intent(out),depend(n) :: a integer intent(in) :: n end subroutine fib end interface end python module fib2
最后,我们通过运行以下命令使用
numpy.distutils
构建扩展模块python -m numpy.f2py -c fib2.pyf fib1.f
在 Python 中
>>> import fib2
>>> print(fib2.fib.__doc__)
a = fib(n)
Wrapper for ``fib``.
Parameters
----------
n : input int
Returns
-------
a : rank-1 array('d') with bounds (n)
>>> print(fib2.fib(8))
[ 0. 1. 1. 2. 3. 5. 8. 13.]
注意
现在,
fib2.fib
的签名更接近 Fortran 子例程FIB
的意图:给定数字n
,fib2.fib
将前n
个斐波那契数作为 NumPy 数组返回。新的 Python 签名fib2.fib
也排除了fib1.fib
中的意外行为。请注意,默认情况下,使用单个
intent(out)
也意味着intent(hide)
。指定了intent(hide)
属性的参数不会在包装函数的参数列表中列出。
有关更多详细信息,请参阅 签名文件。
快速而智能的方法#
如上所述,包装 Fortran 函数的“智能方法”适合于包装(例如第三方)Fortran 代码,这些代码不需要或甚至不可能对其源代码进行修改。
但是,如果可以编辑 Fortran 代码,则在大多数情况下可以跳过生成中间签名文件。可以使用 F2PY 指令将 F2PY 特定的属性直接插入 Fortran 源代码中。F2PY 指令由特殊的注释行组成(例如,以 Cf2py
或 !f2py
开头),这些注释行会被 Fortran 编译器忽略,但会被 F2PY 作为普通行解释。
考虑先前 Fortran 代码的修改版本,其中包含 F2PY 指令,将其保存为 fib3.f
C FILE: FIB3.F
SUBROUTINE FIB(A,N)
C
C CALCULATE FIRST N FIBONACCI NUMBERS
C
INTEGER N
REAL*8 A(N)
Cf2py intent(in) n
Cf2py intent(out) a
Cf2py depend(n) a
DO I=1,N
IF (I.EQ.1) THEN
A(I) = 0.0D0
ELSEIF (I.EQ.2) THEN
A(I) = 1.0D0
ELSE
A(I) = A(I-1) + A(I-2)
ENDIF
ENDDO
END
C END FILE FIB3.F
现在,可以使用单个命令构建扩展模块
python -m numpy.f2py -c -m fib3 fib3.f
请注意,对 FIB
的结果包装程序与之前的情况一样“智能”(无歧义)
>>> import fib3
>>> print(fib3.fib.__doc__)
a = fib(n)
Wrapper for ``fib``.
Parameters
----------
n : input int
Returns
-------
a : rank-1 array('d') with bounds (n)
>>> print(fib3.fib(8))
[ 0. 1. 1. 2. 3. 5. 8. 13.]