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

簡介#

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

double rms(double* seq, int n);

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

如果我們設計了 rms,我們可能會將其設為一個常式,它接受一個長度為 n 的僅輸入陣列,其中包含名為 seqdouble 值,並傳回均方根。然而,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_failgoto 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 引數使用原生位元組順序。如果不是,則會引發例外。

還有一種「扁平」就地陣列,適用於您想要修改或處理每個元素的情況,無論維度數量如何。一個範例是「量化」函式,它就地量化陣列的每個元素,無論是 1D、2D 或任何維度。這種形式檢查連續性,但允許 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 View 陣列#

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 View 陣列#

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 介面檔案由於數個原因,不支援輸出陣列的型別對應 (typemaps)。首先,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 所有維度的跨步 (stridess),假設 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 順序旗標並重新計算跨步。

超出提供的型別對應#

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

常見範例#

考慮點積函式的合理原型

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 指令之前。或者,如果相關函式是類別方法,您將希望除了 %ignore 之外,還使用 %extend 而不是 %inline

關於錯誤處理的注意事項: 請注意,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 版本號為何,都可與 NumPy C/API 搭配使用,無論是在版本 1.6 之後某些 API 方面被棄用之前還是之後。