numpy.i:NumPy 的 SWIG 介面檔案#

簡介#

簡易封裝器與介面產生器(或 SWIG)是個強大的工具,能產生封裝程式碼,以介接各種腳本語言。SWIG 可以解析標頭檔,並僅使用程式碼原型,建立目標語言的介面。但 SWIG 並非萬能。例如,它無法從原型得知

double rms(double* seq, int n);

seq 到底是什麼。它是一個要就地修改的單一值嗎?它是一個陣列嗎?如果是,它的長度是多少?它是僅輸入、僅輸出,還是輸入輸出?SWIG 無法判斷這些細節,也不會嘗試這麼做。

如果我們設計了 rms,我們可能會將其設計為一個常式,它接受一個長度為 ndouble 值輸入陣列,名為 seq,並傳回均方根。然而,SWIG 的預設行為會建立一個可以編譯,但幾乎不可能以 C 常式的預期方式從腳本語言使用的封裝函式。

對於 Python 而言,處理連續(或技術上來說,跨步)同質資料區塊的首選方式是使用 NumPy,它提供對多維資料陣列的完整物件導向存取。因此,rms 函式最合理的 Python 介面會是(包含文件字串)

def rms(seq):
    """
    rms: return the root mean square of a sequence
    rms(numpy.ndarray) -> double
    rms(list) -> double
    rms(tuple) -> double
    """

其中 seq 會是一個 double 值的 NumPy 陣列,而它的長度 n 會在傳遞給 C 常式之前,從 seq 內部提取。更棒的是,由於 NumPy 支援從任意 Python 序列建構陣列,seq 本身幾乎可以是任意序列(只要每個元素都可以轉換為 double),並且封裝程式碼會在內部將其轉換為 NumPy 陣列,然後再提取其資料和長度。

SWIG 允許透過稱為類型映射的機制來定義這些類型的轉換。本文檔提供關於如何使用 numpy.i 的資訊,numpy.i 是一個 SWIG 介面檔案,它定義了一系列旨在使上述陣列相關轉換類型相對容易實作的類型映射。例如,假設上面定義的 rms 函式原型位於名為 rms.h 的標頭檔中。為了獲得上面討論的 Python 介面,您的 SWIG 介面檔案需要包含以下內容

%{
#define SWIG_FILE_WITH_INIT
#include "rms.h"
%}

%include "numpy.i"

%init %{
import_array();
%}

%apply (double* IN_ARRAY1, int DIM1) {(double* seq, int n)};
%include "rms.h"

類型映射會根據一個或多個函式引數的列表進行鍵控,可以是依類型或依類型和名稱。我們會將此類列表稱為簽名numpy.i 定義的眾多類型映射之一在上面使用,並且具有簽名 (double* IN_ARRAY1, int DIM1)。引數名稱旨在表明 double* 引數是一個一維輸入陣列,而 int 代表該維度的大小。這正是 rms 原型中的模式。

最有可能的是,要封裝的實際原型不會具有引數名稱 IN_ARRAY1DIM1。我們使用 SWIG %apply 指令,將一維 double 類型輸入陣列的類型映射應用於 rms 使用的實際原型。因此,有效地使用 numpy.i 需要知道有哪些可用的類型映射以及它們的作用。

包含上面給出的 SWIG 指令的 SWIG 介面檔案將產生如下所示的封裝程式碼

 1 PyObject *_wrap_rms(PyObject *args) {
 2   PyObject *resultobj = 0;
 3   double *arg1 = (double *) 0 ;
 4   int arg2 ;
 5   double result;
 6   PyArrayObject *array1 = NULL ;
 7   int is_new_object1 = 0 ;
 8   PyObject * obj0 = 0 ;
 9
10   if (!PyArg_ParseTuple(args,(char *)"O:rms",&obj0)) SWIG_fail;
11   {
12     array1 = obj_to_array_contiguous_allow_conversion(
13                  obj0, NPY_DOUBLE, &is_new_object1);
14     npy_intp size[1] = {
15       -1
16     };
17     if (!array1 || !require_dimensions(array1, 1) ||
18         !require_size(array1, size, 1)) SWIG_fail;
19     arg1 = (double*) array1->data;
20     arg2 = (int) array1->dimensions[0];
21   }
22   result = (double)rms(arg1,arg2);
23   resultobj = SWIG_From_double((double)(result));
24   {
25     if (is_new_object1 && array1) Py_DECREF(array1);
26   }
27   return resultobj;
28 fail:
29   {
30     if (is_new_object1 && array1) Py_DECREF(array1);
31   }
32   return NULL;
33 }

來自 numpy.i 的類型映射負責以下程式碼行:12–20、25 和 30。第 10 行解析 rms 函式的輸入。從格式字串 "O:rms",我們可以看到引數列表預期為單一 Python 物件(由冒號前的 O 指定),其指標儲存在 obj0 中。由 numpy.i 提供的許多函式被呼叫,以進行和檢查從通用 Python 物件到 NumPy 陣列的(可能)轉換。這些函式在 輔助函式 章節中說明,但希望它們的名稱是不言自明的。在第 12 行,我們使用 obj0 建構 NumPy 陣列。在第 17 行,我們檢查結果的有效性:它是否為非空值,以及它是否具有任意長度的單一維度。一旦驗證了這些狀態,我們就在第 19 行和第 20 行提取資料緩衝區和長度,以便我們可以在第 22 行呼叫底層 C 函式。第 25 行針對我們建立了一個不再需要的新陣列的情況執行記憶體管理。

此程式碼具有大量的錯誤處理。請注意 SWIG_fail 是一個巨集,用於 goto fail,指的是第 28 行的標籤。如果使用者提供的引數數量錯誤,這將在第 10 行被捕獲。如果 NumPy 陣列的建構失敗或產生具有錯誤維度數量的陣列,這些錯誤將在第 17 行被捕獲。最後,如果偵測到錯誤,記憶體仍然在第 30 行正確管理。

請注意,如果 C 函式簽名順序不同

double rms(int n, double* seq);

SWIG 將不會使上面給出的類型映射簽名與 rms 的引數列表匹配。幸運的是,numpy.i 有一組類型映射,其中資料指標放在最後

%apply (int DIM1, double* IN_ARRAY1) {(int n, double* seq)};

這僅僅具有切換上面產生的程式碼的第 3 行和第 4 行中 arg1arg2 的定義,以及它們在第 19 行和第 20 行中的賦值。

使用 numpy.i#

numpy.i 檔案目前位於 numpy 安裝目錄下的 tools/swig 子目錄中。通常,您會希望將其複製到您正在開發封裝器的目錄中。

僅使用單一 SWIG 介面檔案的簡單模組應包含以下內容

%{
#define SWIG_FILE_WITH_INIT
%}
%include "numpy.i"
%init %{
import_array();
%}

在已編譯的 Python 模組中,import_array() 應該只被呼叫一次。這可能在您已撰寫並連結到模組的 C/C++ 檔案中。如果是這種情況,那麼您的介面檔案都不應 #define SWIG_FILE_WITH_INIT 或呼叫 import_array()。或者,此初始化呼叫可能在由 SWIG 從具有如上所示的 %init 區塊的介面檔案產生的封裝器檔案中。如果是這種情況,並且您有多個 SWIG 介面檔案,則只有一個介面檔案應 #define SWIG_FILE_WITH_INIT 並呼叫 import_array()

可用的類型映射#

numpy.i 針對不同資料類型(例如 doubleint)和不同類型維度(例如 intlong)的陣列提供的類型映射指令彼此相同,除了 C 和 NumPy 類型規格之外。因此,類型映射是透過巨集實作的(通常在幕後)

%numpy_typemaps(DATA_TYPE, DATA_TYPECODE, DIM_TYPE)

可以針對適當的 (DATA_TYPE, DATA_TYPECODE, DIM_TYPE) 三元組調用。例如

%numpy_typemaps(double, NPY_DOUBLE, int)
%numpy_typemaps(int,    NPY_INT   , int)

numpy.i 介面檔案使用 %numpy_typemaps 巨集來實作以下 C 資料類型和 int 維度類型的類型映射

  • signed char

  • unsigned char

  • short

  • unsigned short

  • int

  • unsigned int

  • long

  • unsigned long

  • long long

  • unsigned long long

  • float

  • double

在以下說明中,我們參考通用的 DATA_TYPE,它可以是上面列出的任何 C 資料類型,以及 DIM_TYPE,它應該是多種整數類型之一。

類型映射簽名主要根據給定緩衝區指標的名稱來區分。名稱帶有 FARRAY 的是 Fortran 順序陣列,名稱帶有 ARRAY 的是 C 順序陣列(或 1D 陣列)。

輸入陣列#

輸入陣列定義為傳遞到常式中但不會就地修改或傳回給使用者的資料陣列。因此,Python 輸入陣列可以幾乎是任何可以轉換為請求陣列類型的 Python 序列(例如列表)。輸入陣列簽名為

1D

  • (   DATA_TYPE IN_ARRAY1[ANY] )

  • (   DATA_TYPE* IN_ARRAY1, int DIM1 )

  • (   int DIM1, DATA_TYPE* IN_ARRAY1 )

2D

  • (   DATA_TYPE IN_ARRAY2[ANY][ANY] )

  • (   DATA_TYPE* IN_ARRAY2, int DIM1, int DIM2 )

  • (   int DIM1, int DIM2, DATA_TYPE* IN_ARRAY2 )

  • (   DATA_TYPE* IN_FARRAY2, int DIM1, int DIM2 )

  • (   int DIM1, int DIM2, DATA_TYPE* IN_FARRAY2 )

3D

  • (   DATA_TYPE IN_ARRAY3[ANY][ANY][ANY] )

  • (   DATA_TYPE* IN_ARRAY3, int DIM1, int DIM2, int DIM3 )

  • (   int DIM1, int DIM2, int DIM3, DATA_TYPE* IN_ARRAY3 )

  • (   DATA_TYPE* IN_FARRAY3, int DIM1, int DIM2, int DIM3 )

  • (   int DIM1, int DIM2, int DIM3, DATA_TYPE* IN_FARRAY3 )

4D

  • (DATA_TYPE IN_ARRAY4[ANY][ANY][ANY][ANY])

  • (DATA_TYPE* IN_ARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)

  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, , DIM_TYPE DIM4, DATA_TYPE* IN_ARRAY4)

  • (DATA_TYPE* IN_FARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)

  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4, DATA_TYPE* IN_FARRAY4)

第一個列出的簽名 ( DATA_TYPE IN_ARRAY[ANY] ) 用於具有硬編碼維度的一維陣列。同樣地,( DATA_TYPE IN_ARRAY2[ANY][ANY] ) 用於具有硬編碼維度的二維陣列,三維陣列也是如此。

就地陣列#

就地陣列定義為就地修改的陣列。輸入值可能會或可能不會被使用,但函式傳回時的值是重要的。因此,提供的 Python 引數必須是所需類型的 NumPy 陣列。就地簽名為

1D

  • (   DATA_TYPE INPLACE_ARRAY1[ANY] )

  • (   DATA_TYPE* INPLACE_ARRAY1, int DIM1 )

  • (   int DIM1, DATA_TYPE* INPLACE_ARRAY1 )

2D

  • (   DATA_TYPE INPLACE_ARRAY2[ANY][ANY] )

  • (   DATA_TYPE* INPLACE_ARRAY2, int DIM1, int DIM2 )

  • (   int DIM1, int DIM2, DATA_TYPE* INPLACE_ARRAY2 )

  • (   DATA_TYPE* INPLACE_FARRAY2, int DIM1, int DIM2 )

  • (   int DIM1, int DIM2, DATA_TYPE* INPLACE_FARRAY2 )

3D

  • (   DATA_TYPE INPLACE_ARRAY3[ANY][ANY][ANY] )

  • (   DATA_TYPE* INPLACE_ARRAY3, int DIM1, int DIM2, int DIM3 )

  • (   int DIM1, int DIM2, int DIM3, DATA_TYPE* INPLACE_ARRAY3 )

  • (   DATA_TYPE* INPLACE_FARRAY3, int DIM1, int DIM2, int DIM3 )

  • (   int DIM1, int DIM2, int DIM3, DATA_TYPE* INPLACE_FARRAY3 )

4D

  • (DATA_TYPE INPLACE_ARRAY4[ANY][ANY][ANY][ANY])

  • (DATA_TYPE* INPLACE_ARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)

  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, , DIM_TYPE DIM4, DATA_TYPE* INPLACE_ARRAY4)

  • (DATA_TYPE* INPLACE_FARRAY4, DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4)

  • (DIM_TYPE DIM1, DIM_TYPE DIM2, DIM_TYPE DIM3, DIM_TYPE DIM4, DATA_TYPE* INPLACE_FARRAY4)

這些類型映射現在檢查以確保 INPLACE_ARRAY 引數使用原生位元組順序。如果不是,則會引發例外。

還有一個「扁平」就地陣列,適用於您想要修改或處理每個元素的情況,無論維度數量如何。一個範例是「量化」函式,它就地量化陣列的每個元素,無論它是一維、二維或任何維度。此形式檢查連續性,但允許 C 或 Fortran 順序。

ND

  • (DATA_TYPE* INPLACE_ARRAY_FLAT, DIM_TYPE DIM_FLAT)

Argout 陣列#

Argout 陣列是在 C 中的輸入引數中出現,但實際上是輸出陣列的陣列。當有多個輸出變數且單一傳回引數不足時,通常會發生這種模式。在 Python 中,傳回多個引數的傳統方式是將它們封裝到序列(元組、列表等)中並傳回序列。這就是 argout 類型映射的作用。如果使用這些 argout 類型映射的封裝函式有多個傳回引數,則它們會被封裝到元組或列表中,具體取決於 Python 版本。Python 使用者不會傳入這些陣列,它們只會被傳回。對於指定維度的情況,python 使用者必須將該維度作為引數提供。argout 簽名為

1D

  • (   DATA_TYPE ARGOUT_ARRAY1[ANY] )

  • (   DATA_TYPE* ARGOUT_ARRAY1, int DIM1 )

  • (   int DIM1, DATA_TYPE* ARGOUT_ARRAY1 )

2D

  • (   DATA_TYPE ARGOUT_ARRAY2[ANY][ANY] )

3D

  • (   DATA_TYPE ARGOUT_ARRAY3[ANY][ANY][ANY] )

4D

  • (   DATA_TYPE ARGOUT_ARRAY4[ANY][ANY][ANY][ANY] )

這些通常用於在 C/C++ 中,您會在堆積上分配陣列,並呼叫函式來填寫陣列值的情況。在 Python 中,陣列會為您分配,並作為新的陣列物件傳回。

請注意,我們在 1D 中支援 DATA_TYPE* argout 類型映射,但在 2D 或 3D 中則不支援。這是由於 SWIG 類型映射語法的一個怪癖,無法避免。請注意,對於這些類型的 1D 類型映射,Python 函式將採用代表 DIM1 的單一引數。

Argout 檢視陣列#

Argoutview 陣列適用於您的 C 程式碼為您提供其內部資料的檢視,並且不需要使用者分配任何記憶體的情況。這可能很危險。幾乎無法保證來自 C 程式碼的內部資料在封裝它的 NumPy 陣列的整個生命週期內都將保持存在。如果使用者在銷毀 NumPy 陣列之前銷毀了提供資料檢視的物件,則使用該陣列可能會導致錯誤的記憶體參考或區段錯誤。儘管如此,在處理大型資料集的情況下,您根本別無選擇。

要為 argoutview 陣列封裝的 C 程式碼的特徵是指標:指向維度的指標和指向資料的雙指標,以便這些值可以傳回給使用者。因此,argoutview 類型映射簽名為

1D

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY1, DIM_TYPE* DIM1 )

  • ( DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEW_ARRAY1 )

2D

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2 )

  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEW_ARRAY2 )

  • ( DATA_TYPE** ARGOUTVIEW_FARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2 )

  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEW_FARRAY2 )

3D

  • ( DATA_TYPE** ARGOUTVIEW_ARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)

  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEW_ARRAY3)

  • ( DATA_TYPE** ARGOUTVIEW_FARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)

  • ( DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEW_FARRAY3)

4D

  • (DATA_TYPE** ARGOUTVIEW_ARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)

  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEW_ARRAY4)

  • (DATA_TYPE** ARGOUTVIEW_FARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)

  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEW_FARRAY4)

請注意,不支援具有硬編碼維度的陣列。這些不能遵循這些類型映射的雙指標簽名。

記憶體管理 Argout 檢視陣列#

最近新增到 numpy.i 的是允許具有記憶體管理檢視的 argout 陣列的類型映射。

1D

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY1, DIM_TYPE* DIM1)

  • (DIM_TYPE* DIM1, DATA_TYPE** ARGOUTVIEWM_ARRAY1)

2D

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)

  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEWM_ARRAY2)

  • (DATA_TYPE** ARGOUTVIEWM_FARRAY2, DIM_TYPE* DIM1, DIM_TYPE* DIM2)

  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DATA_TYPE** ARGOUTVIEWM_FARRAY2)

3D

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)

  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEWM_ARRAY3)

  • (DATA_TYPE** ARGOUTVIEWM_FARRAY3, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3)

  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DATA_TYPE** ARGOUTVIEWM_FARRAY3)

4D

  • (DATA_TYPE** ARGOUTVIEWM_ARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)

  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEWM_ARRAY4)

  • (DATA_TYPE** ARGOUTVIEWM_FARRAY4, DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4)

  • (DIM_TYPE* DIM1, DIM_TYPE* DIM2, DIM_TYPE* DIM3, DIM_TYPE* DIM4, DATA_TYPE** ARGOUTVIEWM_FARRAY4)

輸出陣列#

numpy.i 介面檔案不支援輸出陣列的類型映射,原因有幾個。首先,C/C++ 傳回引數僅限於單一值。這阻止了以通用方式獲取維度資訊。其次,不允許將具有硬編碼長度的陣列作為傳回引數。換句話說

double[3] newVector(double x, double y, double z);

不是合法的 C/C++ 語法。因此,我們無法提供以下形式的類型映射

%typemap(out) (TYPE[ANY]);

如果您遇到函式或方法傳回陣列指標的情況,最好的方法是編寫您自己的函式版本以進行包裝,類別方法的情況使用 %extend,函式的情況使用 %ignore%rename

其他常見類型:bool#

請注意,可用類型映射章節的列表中不支援 C++ 類型 bool。NumPy 布林值是單一位元組,而 C++ bool 是四個位元組(至少在我的系統上是這樣)。因此

%numpy_typemaps(bool, NPY_BOOL, int)

將導致類型映射產生參考不正確資料長度的程式碼。您可以實作以下巨集展開

%numpy_typemaps(bool, NPY_UINT, int)

以修正資料長度問題,而 輸入陣列 將運作正常,但 原地陣列 可能會類型檢查失敗。

其他常見類型:complex#

複數浮點數類型的類型映射轉換也不會自動支援。這是因為 Python 和 NumPy 是用 C 語言編寫的,而 C 語言沒有原生複數類型。Python 和 NumPy 都實作了它們自己的(基本上等效的)struct 複數變數定義

/* Python */
typedef struct {double real; double imag;} Py_complex;

/* NumPy */
typedef struct {float  real, imag;} npy_cfloat;
typedef struct {double real, imag;} npy_cdouble;

我們可以實作

%numpy_typemaps(Py_complex , NPY_CDOUBLE, int)
%numpy_typemaps(npy_cfloat , NPY_CFLOAT , int)
%numpy_typemaps(npy_cdouble, NPY_CDOUBLE, int)

這將為 Py_complexnpy_cfloatnpy_cdouble 類型的陣列提供自動類型轉換。然而,似乎不太可能有任何獨立(非 Python、非 NumPy)的應用程式碼,人們會使用 SWIG 來產生 Python 介面,並且也使用了這些複數類型定義。更可能的是,這些應用程式碼將定義它們自己的複數類型,或者在 C++ 的情況下,使用 std::complex。假設這些資料結構與 Python 和 NumPy 複數類型相容,則如上的 %numpy_typemap 展開(使用使用者的複數類型替換第一個引數)應該可以運作。

NumPy 陣列純量和 SWIG#

SWIG 對數值類型具有複雜的類型檢查。例如,如果您的 C/C++ 常式預期整數作為輸入,則 SWIG 產生的程式碼將檢查 Python 整數和 Python 長整數,如果提供的 Python 整數太大而無法向下轉換為 C 整數,則會引發溢位錯誤。隨著 NumPy 純量陣列引入您的 Python 程式碼,您可能會從 NumPy 陣列中提取整數,並嘗試將其傳遞給 SWIG 包裝的 C/C++ 函式,該函式預期 int,但 SWIG 類型檢查將無法將 NumPy 陣列純量識別為整數。(通常,這實際上確實有效 – 這取決於 NumPy 是否將您使用的整數類型識別為繼承自您正在使用的平台上的 Python 整數類型。有時,這表示在 32 位元機器上運作的程式碼在 64 位元機器上會失敗。)

如果您收到類似以下的 Python 錯誤

TypeError: in method 'MyClass_MyMethod', argument 2 of type 'int'

並且您傳遞的引數是從 NumPy 陣列中提取的整數,那麼您就遇到了這個問題。解決方案是修改 SWIG 類型轉換系統,使其除了標準整數類型之外,還接受 NumPy 陣列純量。幸運的是,已經為您提供了此功能。只需複製檔案

pyfragments.swg

到您專案的工作建置目錄,這個問題就會得到解決。建議您無論如何都這樣做,因為它只會增加 Python 介面的功能。

為什麼有第二個檔案?#

SWIG 類型檢查和轉換系統是 C 巨集、SWIG 巨集、SWIG 類型映射和 SWIG 片段的複雜組合。片段是一種在需要時有條件地將程式碼插入您的包裝器檔案中,而不需要時則不插入的方法。如果多個類型映射需要相同的片段,則該片段只會插入您的包裝器程式碼一次。

有一個將 Python 整數轉換為 C long 的片段。還有一個不同的片段,將 Python 整數轉換為 C int,它呼叫 long 片段中定義的常式。我們可以透過更改 long 片段的定義來進行我們想要的更改。SWIG 使用「先到先得」系統來決定片段的有效定義。也就是說,我們需要在 SWIG 內部執行 long 轉換的片段之前定義它。SWIG 允許我們透過將片段定義放在 pyfragments.swg 檔案中來做到這一點。如果我們將新的片段定義放在 numpy.i 中,它們將被忽略。

輔助函式#

numpy.i 檔案包含多個巨集和常式,它在內部使用這些巨集和常式來建置其類型映射。但是,這些函式在您的介面檔案中的其他地方可能也很有用。這些巨集和常式是作為片段實作的,在上一節中簡要描述了片段。如果您嘗試使用以下一個或多個巨集或函式,但您的編譯器抱怨它無法識別符號,那麼您需要強制這些片段使用以下程式碼出現在您的程式碼中

%fragment("NumPy_Fragments");

在您的 SWIG 介面檔案中。

巨集#

is_array(a)

如果 a 為非 NULL 且可以轉換為 PyArrayObject*,則評估為 true。

array_type(a)

評估為 a 的整數資料類型程式碼,假設 a 可以轉換為 PyArrayObject*

array_numdims(a)

評估為 a 的整數維度數,假設 a 可以轉換為 PyArrayObject*

array_dimensions(a)

評估為 npy_intp 類型且長度為 array_numdims(a) 的陣列,給出 a 所有維度的長度,假設 a 可以轉換為 PyArrayObject*

array_size(a,i)

評估為 a 的第 i 個維度大小,假設 a 可以轉換為 PyArrayObject*

array_strides(a)

評估為 npy_intp 類型且長度為 array_numdims(a) 的陣列,給出 a 所有維度的步幅,假設 a 可以轉換為 PyArrayObject*。步幅是元素與沿同一軸的直接鄰居之間的位元組距離。

array_stride(a,i)

評估為 a 的第 i 個步幅,假設 a 可以轉換為 PyArrayObject*

array_data(a)

評估為 void* 類型的指標,該指標指向 a 的資料緩衝區,假設 a 可以轉換為 PyArrayObject*

array_descr(a)

傳回對 a 的 dtype 屬性(PyArray_Descr*)的借用參考,假設 a 可以轉換為 PyArrayObject*

array_flags(a)

傳回表示 a 旗標的整數,假設 a 可以轉換為 PyArrayObject*

array_enableflags(a,f)

設定 af 表示的旗標,假設 a 可以轉換為 PyArrayObject*

array_is_contiguous(a)

如果 a 是連續陣列,則評估為 true。等效於 (PyArray_ISCONTIGUOUS(a))

array_is_native(a)

如果 a 的資料緩衝區使用原生位元組順序,則評估為 true。等效於 (PyArray_ISNOTSWAPPED(a))

array_is_fortran(a)

如果 a 是 FORTRAN 順序,則評估為 true。

常式#

pytype_string()

傳回類型:const char*

引數

  • PyObject* py_obj,一般的 Python 物件。

傳回描述 py_obj 類型的字串。

typecode_string()

傳回類型:const char*

引數

  • int typecode,NumPy 整數類型代碼。

傳回描述與 NumPy typecode 對應的類型的字串。

type_match()

傳回類型:int

引數

  • int actual_type,NumPy 陣列的 NumPy 類型代碼。

  • int desired_type,所需的 NumPy 類型代碼。

確保 actual_typedesired_type 相容。例如,這允許字元和位元組類型,或 int 和 long 類型,進行匹配。這現在等效於 PyArray_EquivTypenums()

obj_to_array_no_conversion()

傳回類型:PyArrayObject*

引數

  • PyObject* input,一般的 Python 物件。

  • int typecode,所需的 NumPy 類型代碼。

如果合法,將 input 轉換為 PyArrayObject*,並確保它是 typecode 類型。如果 input 無法轉換,或 typecode 錯誤,則設定 Python 錯誤並傳回 NULL

obj_to_array_allow_conversion()

傳回類型:PyArrayObject*

引數

  • PyObject* input,一般的 Python 物件。

  • int typecode,結果陣列所需的 NumPy 類型代碼。

  • int* is_new_object,如果未執行轉換,則傳回值 0,否則傳回 1。

input 轉換為具有給定 typecode 的 NumPy 陣列。成功時,傳回具有正確類型的有效 PyArrayObject*。失敗時,將設定 Python 錯誤字串,並且常式傳回 NULL

make_contiguous()

傳回類型:PyArrayObject*

引數

  • PyArrayObject* ary,NumPy 陣列。

  • int* is_new_object,如果未執行轉換,則傳回值 0,否則傳回 1。

  • int min_dims,最小允許維度。

  • int max_dims,最大允許維度。

檢查 ary 是否連續。如果是,則傳回輸入指標並將其標記為非新物件。如果它不連續,則使用原始資料建立新的 PyArrayObject*,將其標記為新物件並傳回指標。

make_fortran()

傳回類型:PyArrayObject*

引數

  • PyArrayObject* ary,NumPy 陣列。

  • int* is_new_object,如果未執行轉換,則傳回值 0,否則傳回 1。

檢查 ary 是否為 Fortran 連續。如果是,則傳回輸入指標並將其標記為非新物件。如果它不是 Fortran 連續,則使用原始資料建立新的 PyArrayObject*,將其標記為新物件並傳回指標。

obj_to_array_contiguous_allow_conversion()

傳回類型:PyArrayObject*

引數

  • PyObject* input,一般的 Python 物件。

  • int typecode,結果陣列所需的 NumPy 類型代碼。

  • int* is_new_object,如果未執行轉換,則傳回值 0,否則傳回 1。

input 轉換為指定類型的連續 PyArrayObject*。如果輸入物件不是連續 PyArrayObject*,則將建立一個新的物件,並且將設定新物件旗標。

obj_to_array_fortran_allow_conversion()

傳回類型:PyArrayObject*

引數

  • PyObject* input,一般的 Python 物件。

  • int typecode,結果陣列所需的 NumPy 類型代碼。

  • int* is_new_object,如果未執行轉換,則傳回值 0,否則傳回 1。

input 轉換為指定類型的 Fortran 連續 PyArrayObject*。如果輸入物件不是 Fortran 連續 PyArrayObject*,則將建立一個新的物件,並且將設定新物件旗標。

require_contiguous()

傳回類型:int

引數

  • PyArrayObject* ary,NumPy 陣列。

測試 ary 是否連續。如果是,則傳回 1。否則,設定 Python 錯誤並傳回 0。

require_native()

傳回類型:int

引數

  • PyArray_Object* ary,NumPy 陣列。

要求 ary 不是位元組交換的。如果陣列不是位元組交換的,則傳回 1。否則,設定 Python 錯誤並傳回 0。

require_dimensions()

傳回類型:int

引數

  • PyArrayObject* ary,NumPy 陣列。

  • int exact_dimensions,所需的維度數。

要求 ary 具有指定的維度數。如果陣列具有指定的維度數,則傳回 1。否則,設定 Python 錯誤並傳回 0。

require_dimensions_n()

傳回類型:int

引數

  • PyArrayObject* ary,NumPy 陣列。

  • int* exact_dimensions,表示可接受的維度數的整數陣列。

  • int nexact_dimensions 的長度。

要求 ary 具有指定維度數列表中的其中一個。如果陣列具有指定維度數中的其中一個,則傳回 1。否則,設定 Python 錯誤字串並傳回 0。

require_size()

傳回類型:int

引數

  • PyArrayObject* ary,NumPy 陣列。

  • npy_int* size,表示每個維度所需長度的陣列。

  • int nsize 的長度。

要求 ary 具有指定的形狀。如果陣列具有指定的形狀,則傳回 1。否則,設定 Python 錯誤字串並傳回 0。

require_fortran()

傳回類型:int

引數

  • PyArrayObject* ary,NumPy 陣列。

要求給定的 PyArrayObject 是 Fortran 順序。如果 PyArrayObject 已經是 Fortran 順序,則不執行任何操作。否則,設定 Fortran 順序旗標並重新計算步幅。

超出提供的類型映射#

有許多簡單的 %include "numpy.i" 和後續的 %apply 指令未涵蓋的 C 或 C++ 陣列/NumPy 陣列情況。

常見範例#

考慮點積函式的合理原型

double dot(int len, double* vec1, double* vec2);

我們想要的 Python 介面是

def dot(vec1, vec2):
    """
    dot(PyObject,PyObject) -> double
    """

這裡的問題是有一個維度引數和兩個陣列引數,而我們的類型映射是為適用於單個陣列的維度設定的(事實上,SWIG 沒有提供將 lenvec2 關聯的機制,這需要兩個 Python 輸入引數)。建議的解決方案如下

%apply (int DIM1, double* IN_ARRAY1) {(int len1, double* vec1),
                                      (int len2, double* vec2)}
%rename (dot) my_dot;
%exception my_dot {
    $action
    if (PyErr_Occurred()) SWIG_fail;
}
%inline %{
double my_dot(int len1, double* vec1, int len2, double* vec2) {
    if (len1 != len2) {
        PyErr_Format(PyExc_ValueError,
                     "Arrays of lengths (%d,%d) given",
                     len1, len2);
        return 0.0;
    }
    return dot(len1, vec1, vec2);
}
%}

如果包含 double dot() 原型的標頭檔也包含您想要包裝的其他原型,因此您需要 %include 這個標頭檔,那麼您還需要 %ignore dot; 指令,放在 %rename 之後和 %include 指令之前。或者,如果所討論的函式是類別方法,您將想要使用 %extend 而不是 %inline,以及 %ignore

關於錯誤處理的注意事項: 請注意,my_dot 傳回 double,但它也可能引發 Python 錯誤。當向量長度不匹配時,產生的包裝器函式將傳回 0.0 的 Python float 表示形式。由於這不是 NULL,Python 解譯器將不知道檢查錯誤。因此,我們在上面為 my_dot 新增了 %exception 指令,以獲得我們想要的行為(請注意,$action 是一個巨集,它會展開為對 my_dot 的有效呼叫)。一般來說,您可能需要編寫一個 SWIG 巨集來執行此任務。

其他情況#

在其他包裝情況下,當您遇到它們時,numpy.i 可能會有幫助。

  • 在某些情況下,您可以使用 %numpy_typemaps 巨集為您自己的類型實作類型映射。請參閱 其他常見類型:bool其他常見類型:complex 章節以取得範例。另一種情況是,如果您的維度是 int 以外的類型(例如 long

    %numpy_typemaps(double, NPY_DOUBLE, long)
    
  • 您可以使用 numpy.i 中的程式碼來編寫您自己的類型映射。例如,如果您有一個五維陣列作為函式引數,您可以將適當的四維類型映射剪下並貼到您的介面檔案中。對第四維度的修改將是微不足道的。

  • 有時,最好的方法是使用 %extend 指令為您的類別定義新方法(或覆寫現有方法),這些方法採用 PyObject*(可以是或可以轉換為 PyArrayObject*)而不是指向緩衝區的指標。在這種情況下,numpy.i 中的輔助常式可能非常有用。

  • 編寫類型映射可能有點不直觀。如果您對為 NumPy 編寫 SWIG 類型映射有具體問題,numpy.i 的開發人員會監控 Numpy-discussionSwig-user 郵件列表。

最後的注意事項#

當您使用 %apply 指令時,就像通常使用 numpy.i 所必需的那樣,它將保持有效,直到您告訴 SWIG 它不應該有效為止。如果您的包裝函式或方法的引數具有通用名稱,例如 lengthvector,則這些類型映射可能會應用於您不期望或不想要的情況。因此,在您完成特定的類型映射後新增 %clear 指令始終是一個好主意

%apply (double* IN_ARRAY1, int DIM1) {(double* vector, int length)}
%include "my_header.h"
%clear (double* vector, int length);

一般來說,您應該針對您想要它們的特定類型映射簽章,然後在完成後清除它們。

摘要#

開箱即用,numpy.i 提供支援 NumPy 陣列和 C 陣列之間轉換的類型映射

  • 可以是 12 種不同的純量類型之一:signed charunsigned charshortunsigned shortintunsigned intlongunsigned longlong longunsigned long longfloatdouble

  • 支援每種資料類型 74 種不同的引數簽章,包括

    • 一維、二維、三維和四維陣列。

    • 僅輸入、原地、argout、argoutview 和記憶體管理的 argoutview 行為。

    • 硬式編碼維度、資料緩衝區然後維度規格和維度然後資料緩衝區規格。

    • 2D、3D 和 4D 陣列同時支援 C 順序(「最後一個維度最快」)或 Fortran 順序(「第一個維度最快」)。

numpy.i 介面檔案還為包裝器開發人員提供了其他工具,包括

  • 一個 SWIG 巨集 (%numpy_typemaps),帶有三個引數,用於為使用者選擇的 (1) C 資料類型、(2) NumPy 資料類型(假設它們匹配)和 (3) 維度類型實作 74 個引數簽章。

  • 十四個 C 巨集和十五個 C 函式,可用於編寫專門的類型映射、擴充功能或內嵌函式,以處理提供的類型映射未涵蓋的情況。請注意,巨集和函式經過專門編碼,可與 NumPy C/API 搭配使用,而與 NumPy 版本號碼無關,無論是在 1.6 版之後棄用 API 的某些方面之前還是之後。