三種封裝方法 - 入門#

使用 F2PY 將 Fortran 或 C 函數封裝到 Python 包含以下步驟

  • 建立所謂的簽名檔,其中包含 Fortran 或 C 函數的封裝器描述,也稱為函數的簽名。對於 Fortran 常式,F2PY 可以通過掃描 Fortran 原始碼並追蹤建立封裝函數所需的所有相關資訊來建立初始簽名檔。

    • 可選地,可以編輯 F2PY 建立的簽名檔以優化封裝函數,這可以使它們更「聰明」和更「Python 化」。

  • 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 實作相關引數之間的基本相容性檢查,以避免意外崩潰。

  • 當 NumPy 陣列是 Fortran 連續 且具有與假定的 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.]
    

    但是,將 Fortran 副程式所做的更改傳播到 Python 的建議方法是使用 intent(out) 屬性。這種方法更有效率,也更簡潔。

  • 在 Python 中使用 fib1.fib 與在 Fortran 中使用 FIB 非常相似。但是,在 Python 中使用就地輸出引數是一種不良風格,因為 Python 中沒有安全機制來防止錯誤的引數類型。使用 Fortran 或 C 時,編譯器會在編譯過程中發現任何類型不符的情況,但在 Python 中,必須在執行階段檢查類型。因此,在 Python 中使用就地輸出引數可能會導致難以發現的錯誤,更不用說當實作所有必需的類型檢查時,程式碼的可讀性會降低。

儘管到目前為止討論的用於封裝 Python 的 Fortran 常式的方法非常簡單,但它有幾個缺點(請參閱上面的註解)。缺點是由於 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 的意圖:給定數字 nfib2.fib 以 NumPy 陣列的形式傳回前 n 個費波那契數。新的 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.]