三种封装方法 - 快速入门#

使用 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 的发行版中,此包应称为 python3-dev,在基于 Fedora 的发行版中,它是 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 连续 的 NumPy 数组并且具有与假定的 Fortran 类型对应的 dtype 作为输入数组参数时,则其 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.]
    

    但是,推荐的方法是使用 intent(out) 属性来使 Fortran 子例程所做的更改传播到 Python。这种方法更高效,也更简洁。

  • 在 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 函数FIBa的内容,应该返回到 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的意图:给定数字nfib2.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.]