使用 Python 作為膠水#
警告
這份文件於 2008 年撰寫,是 Travis E. Oliphant 所著原始NumPy 指南書籍的一部分,現已過時。
許多人喜歡說 Python 是一種很棒的膠水語言。希望本章能讓您相信這是真的。最早採用 Python 進行科學運算的人通常是使用它將在超級電腦上運行的龐大應用程式碼膠合在一起的人。Python 不僅比 shell script 或 Perl 更容易編碼,而且輕鬆擴充 Python 的能力使得創建專門針對要解決的問題的新類別和類型相對容易。從這些早期貢獻者的互動中,Numeric 作為一種陣列狀物件出現,可以用於在這些應用程式之間傳遞資料。
隨著 Numeric 的成熟和發展成為 NumPy,人們已經能夠直接在 NumPy 中編寫更多程式碼。通常,這些程式碼對於生產使用來說已經夠快了,但仍然有時候需要存取編譯後的程式碼。無論是為了從演算法中獲得最後一點效率,還是為了更容易存取以 C/C++ 或 Fortran 編寫的廣泛可用的程式碼。
本章將回顧許多可用於存取以其他編譯語言編寫的程式碼的工具。有很多資源可用於學習如何從 Python 呼叫其他編譯後的函式庫,本章的目的不是要讓您成為專家。主要目標是讓您了解一些可能性,以便您知道要「Google」什麼來了解更多資訊。
從 Python 呼叫其他編譯後的函式庫#
雖然 Python 是一種很棒的語言,並且編碼起來很愉快,但它的動態特性導致了額外開銷,這可能會導致某些程式碼(即 for 迴圈內部的原始計算)比用靜態編譯語言編寫的等效程式碼慢 10-100 倍。此外,它可能會導致記憶體使用量比必要的更大,因為在計算過程中會建立和銷毀臨時陣列。對於許多類型的計算需求,額外的減速和記憶體消耗通常是不可避免的(至少對於時間或記憶體關鍵部分的程式碼而言)。因此,最常見的需求之一是從 Python 程式碼呼叫快速的機器碼常式(例如,使用 C/C++ 或 Fortran 編譯)。事實上,這相對容易做到,這是 Python 成為科學和工程程式設計如此出色的高階語言的一個重要原因。
呼叫編譯後程式碼有兩種基本方法:編寫一個擴充模組,然後使用 import 命令將其匯入到 Python 中,或者使用 ctypes 模組直接從 Python 呼叫共享函式庫子常式。編寫擴充模組是最常用的方法。
警告
如果您不小心,從 Python 呼叫 C 程式碼可能會導致 Python 崩潰。本章中的任何方法都不是免疫的。您必須了解一些關於 NumPy 和所使用的第三方函式庫如何處理資料的方式。
手動產生的封裝器#
擴充模組已在 編寫擴充模組 中討論過。與編譯後的程式碼介面的最基本方法是編寫一個擴充模組,並建構一個呼叫編譯後程式碼的模組方法。為了提高可讀性,您的方法應利用 PyArg_ParseTuple
呼叫在 Python 物件和 C 資料類型之間進行轉換。對於標準 C 資料類型,可能已經有一個內建的轉換器。對於其他類型,您可能需要編寫自己的轉換器並使用 "O&"
格式字串,它允許您指定一個函數,該函數將用於執行從 Python 物件到所需任何 C 結構的轉換。
一旦執行了到適當 C 結構和 C 資料類型的轉換,封裝器中的下一步是呼叫底層函數。如果底層函數是用 C 或 C++ 編寫的,這很簡單。但是,為了呼叫 Fortran 程式碼,您必須熟悉如何使用您的編譯器和平台從 C/C++ 呼叫 Fortran 子常式。這在平台和編譯器之間可能會有所不同(這也是 f2py 使 Fortran 程式碼介面更容易的另一個原因),但通常涉及名稱的底線修飾以及所有變數都透過引用傳遞的事實(即,所有引數都是指標)。
手動產生封裝器的優點是您可以完全控制 C 函式庫的使用和呼叫方式,這可以產生精簡且緊密的介面,並將額外開銷降到最低。缺點是您必須編寫、偵錯和維護 C 程式碼,儘管大多數程式碼可以使用從其他擴充模組「剪切-貼上-修改」這種歷史悠久的技術來改編。由於呼叫額外 C 程式碼的過程相當有規律,因此已經開發出程式碼產生程序,以使此過程更容易。其中一種程式碼產生技術隨 NumPy 一起發布,並允許與 Fortran 和(簡單)C 程式碼輕鬆整合。這個套件 f2py 將在下一節中簡要介紹。
F2PY#
F2PY 允許您自動建構一個擴充模組,該模組介接到 Fortran 77/90/95 程式碼中的常式。它能夠剖析 Fortran 77/90/95 程式碼,並自動為它遇到的子常式產生 Python 簽名,或者您可以透過建構介面定義檔案(或修改 f2py 產生的檔案)來指導子常式如何與 Python 介面。
有關更多資訊和範例,請參閱 F2PY 文件。
目前,連結編譯後程式碼的 f2py 方法是最複雜和整合的方法。它允許將 Python 與編譯後的程式碼乾淨地分離,同時仍然允許單獨發布擴充模組。唯一的缺點是它需要 Fortran 編譯器才能讓使用者安裝程式碼。但是,隨著免費編譯器 g77、gfortran 和 g95 以及高品質商業編譯器的存在,這種限制並不是很繁瑣。我們認為,Fortran 仍然是編寫快速且清晰的科學計算程式碼的最簡單方法。它以最直接的方式處理複數和多維索引。但是請注意,某些 Fortran 編譯器將無法像好的手寫 C 程式碼那樣最佳化程式碼。
Cython#
Cython 是一種 Python 方言的編譯器,它增加了(可選的)靜態類型以提高速度,並允許將 C 或 C++ 程式碼混合到您的模組中。它產生 C 或 C++ 擴充,可以編譯並匯入到 Python 程式碼中。
如果您要編寫一個擴充模組,其中也將包含相當多的您自己的演算法程式碼,那麼 Cython 是一個很好的選擇。它的功能之一是能夠輕鬆快速地處理多維陣列。
請注意,Cython 僅是一個擴充模組產生器。與 f2py 不同,它不包含用於編譯和連結擴充模組的自動工具(必須以通常的方式完成)。它確實提供了一個修改後的 distutils 類別,稱為 build_ext
,它允許您從 .pyx
原始碼建置擴充模組。因此,您可以在 setup.py
檔案中編寫
from Cython.Distutils import build_ext
from distutils.extension import Extension
from distutils.core import setup
import numpy
setup(name='mine', description='Nothing',
ext_modules=[Extension('filter', ['filter.pyx'],
include_dirs=[numpy.get_include()])],
cmdclass = {'build_ext':build_ext})
當然,只有當您在擴充模組中使用 NumPy 陣列時(這也是我們假設您使用 Cython 的原因),才需要新增 NumPy 包含目錄。NumPy 中的 distutils 擴充也包括自動產生擴充模組並從 .pyx
檔案連結它的支援。它的工作方式是,如果使用者沒有安裝 Cython,那麼它會尋找一個具有相同檔案名稱但副檔名為 .c
的檔案,然後使用它而不是嘗試再次產生 .c
檔案。
如果您僅使用 Cython 編譯標準 Python 模組,那麼您將獲得一個 C 擴充模組,該模組的運行速度通常比等效的 Python 模組快一點。透過使用 cdef
關鍵字靜態定義 C 變數,可以進一步提高速度。
讓我們看看我們之前看過的兩個範例,看看如何使用 Cython 實作它們。這些範例已使用 Cython 0.21.1 編譯為擴充模組。
Cython 中的複數加法#
以下是名為 add.pyx
的 Cython 模組的一部分,它實作了我們先前使用 f2py 實作的複數加法函數
cimport cython
cimport numpy as np
import numpy as np
# We need to initialize NumPy.
np.import_array()
#@cython.boundscheck(False)
def zadd(in1, in2):
cdef double complex[:] a = in1.ravel()
cdef double complex[:] b = in2.ravel()
out = np.empty(a.shape[0], np.complex64)
cdef double complex[:] c = out.ravel()
for i in range(c.shape[0]):
c[i].real = a[i].real + b[i].real
c[i].imag = a[i].imag + b[i].imag
return out
此模組顯示了 cimport
語句的使用,以從 Cython 隨附的 numpy.pxd
標頭載入定義。NumPy 似乎匯入了兩次;cimport
僅使 NumPy C-API 可用,而常規 import
會在運行時導致 Python 樣式的匯入,並使其可以呼叫熟悉的 NumPy Python API。
該範例還示範了 Cython 的「類型化記憶體檢視」,它們就像 C 級別的 NumPy 陣列,因為它們是形狀和步幅陣列,它們知道自己的範圍(與透過裸指標尋址的 C 陣列不同)。語法 double complex[:]
表示雙精度浮點數的一維陣列(向量),具有任意步幅。整數的連續陣列將是 int[::1]
,而浮點數的矩陣將是 float[:, :]
。
註解中顯示的是 cython.boundscheck
裝飾器,它以每個函數為基礎開啟或關閉記憶體檢視存取的邊界檢查。我們可以利用這一點來進一步加速我們的程式碼,但以犧牲安全性(或在進入迴圈之前進行手動檢查)為代價。
除了檢視語法外,Python 程式設計師可以立即讀懂該函數。變數 i
的靜態類型是隱含的。除了檢視語法外,我們也可以使用 Cython 特殊的 NumPy 陣列語法,但檢視語法是首選。
Cython 中的影像濾波器#
我們使用 Fortran 建立的二維範例在 Cython 中編寫起來同樣容易
cimport numpy as np
import numpy as np
np.import_array()
def filter(img):
cdef double[:, :] a = np.asarray(img, dtype=np.double)
out = np.zeros(img.shape, dtype=np.double)
cdef double[:, ::1] b = out
cdef np.npy_intp i, j
for i in range(1, a.shape[0] - 1):
for j in range(1, a.shape[1] - 1):
b[i, j] = (a[i, j]
+ .5 * ( a[i-1, j] + a[i+1, j]
+ a[i, j-1] + a[i, j+1])
+ .25 * ( a[i-1, j-1] + a[i-1, j+1]
+ a[i+1, j-1] + a[i+1, j+1]))
return out
這個二維平均濾波器運行速度很快,因為迴圈在 C 中,並且指標計算僅在需要時才完成。如果上面的程式碼編譯為模組 image
,那麼可以使用以下程式碼非常快速地過濾二維影像 img
import image
out = image.filter(img)
關於程式碼,有兩件事值得注意:首先,不可能將記憶體檢視傳回給 Python。相反,首先建立 NumPy 陣列 out
,然後將此陣列上的檢視 b
用於計算。其次,檢視 b
的類型為 double[:, ::1]
。這表示具有連續行的二維陣列,即 C 矩陣順序。明確指定順序可以加速某些演算法,因為它們可以跳過步幅計算。
結論#
Cython 是幾個科學 Python 函式庫(包括 Scipy、Pandas、SAGE、scikit-image 和 scikit-learn,以及 XML 處理函式庫 LXML)的首選擴充機制。該語言和編譯器維護良好。
使用 Cython 有幾個缺點
在編碼自訂演算法時,有時在封裝現有的 C 函式庫時,需要具備一些 C 語言的知識。特別是,當使用 C 記憶體管理(
malloc
和 friends)時,很容易引入記憶體洩漏。但是,僅僅將重新命名為.pyx
的 Python 模組編譯就可以加速它,並且新增一些類型宣告可以在某些程式碼中帶來顯著的加速。很容易失去 Python 和 C 之間的乾淨分離,這使得為其他非 Python 相關專案重新使用您的 C 程式碼變得更加困難。
Cython 產生的 C 程式碼難以閱讀和修改(並且通常在編譯時會產生煩人但無害的警告)。
Cython 產生的擴充模組的一個主要優點是它們易於發布。總之,Cython 是一個非常強大的工具,可用於膠合 C 程式碼或快速產生擴充模組,不應被忽視。它對於那些不能或不願意編寫 C 或 Fortran 程式碼的人來說尤其有用。
ctypes#
ctypes 是一個 Python 擴充模組,包含在 stdlib 中,它允許您直接從 Python 呼叫共享函式庫中的任意函數。這種方法允許您直接從 Python 與 C 程式碼介面。這為從 Python 使用大量的函式庫打開了大門。但是,缺點是編碼錯誤很容易導致糟糕的程式崩潰(就像在 C 中可能發生的那樣),因為對參數進行的類型或邊界檢查很少。當陣列資料作為指向原始記憶體位置的指標傳遞時,尤其如此。然後,您有責任確保子常式不會存取實際陣列區域之外的記憶體。但是,如果您不介意冒一點風險,ctypes 可以成為快速利用大型共享函式庫(或在您自己的共享函式庫中編寫擴充功能)的有效工具。
由於 ctypes 方法公開了編譯後程式碼的原始介面,因此它並不總是能容忍使用者的錯誤。ctypes 模組的穩健使用通常涉及額外的 Python 程式碼層,以便檢查傳遞給底層子常式的物件的資料類型和陣列邊界。這種額外的檢查層(更不用說從 ctypes 物件到 ctypes 本身執行的 C 資料類型的轉換)將使介面比手寫的擴充模組介面慢。但是,如果被呼叫的 C 常式正在執行任何大量工作,則這種額外開銷應該可以忽略不計。如果您是一位 C 技能薄弱的出色 Python 程式設計師,ctypes 是一種編寫與編譯後程式碼的(共享)函式庫的有用介面的簡單方法。
要使用 ctypes,您必須
擁有一個共享函式庫。
載入共享函式庫。
將 Python 物件轉換為 ctypes 理解的引數。
使用 ctypes 引數從函式庫呼叫函數。
轉換引數#
Python ints/longs、字串和 unicode 物件會根據需要自動轉換為等效的 ctypes 引數。None 物件也會自動轉換為 NULL 指標。所有其他 Python 物件都必須轉換為 ctypes 特定類型。有兩種方法可以繞過此限制,使 ctypes 可以與其他物件整合。
不要設定函數物件的 argtypes 屬性,並為您要傳入的物件定義
_as_parameter_
方法。_as_parameter_
方法必須傳回一個 Python int,它將直接傳遞給函數。將 argtypes 屬性設定為一個列表,其條目包含具有名為 from_param 的類別方法物件,該方法知道如何將您的物件轉換為 ctypes 可以理解的物件(int/long、字串、unicode 或具有
_as_parameter_
屬性的物件)。
NumPy 使用這兩種方法,並且偏好第二種方法,因為它可能更安全。ndarray 的 ctypes 屬性傳回一個具有 _as_parameter_
屬性的物件,該屬性傳回一個整數,表示與其關聯的 ndarray 的位址。因此,可以將此 ctypes 屬性物件直接傳遞給期望指向 ndarray 中資料的指標的函數。呼叫者必須確保 ndarray 物件的類型、形狀正確,並且已設定正確的旗標,否則如果傳遞了指向不適當陣列的資料指標,則可能會導致嚴重的崩潰。
為了實作第二種方法,NumPy 在 numpy.ctypeslib
模組中提供了類別工廠函數 ndpointer
。此類別工廠函數產生一個適當的類別,可以放置在 ctypes 函數的 argtypes 屬性條目中。該類別將包含一個 from_param 方法,ctypes 將使用該方法將傳遞給函數的任何 ndarray 轉換為 ctypes 可識別的物件。在此過程中,轉換將對使用者在呼叫 ndpointer
時指定的 ndarray 的任何屬性執行檢查。可以檢查的 ndarray 的方面包括資料類型、維度數、形狀和/或傳遞的任何陣列上的旗標狀態。from_param 方法的傳回值是陣列的 ctypes 屬性,ctypes 可以直接使用它(因為它包含指向陣列資料區域的 _as_parameter_
屬性)。
ndarray 的 ctypes 屬性還被賦予了額外的屬性,這些屬性在將有關陣列的其他資訊傳遞到 ctypes 函數時可能很方便。屬性 data、shape 和 strides 可以提供與陣列的資料區域、形狀和步幅相對應的 ctypes 相容類型。data 屬性傳回一個 c_void_p
,表示指向資料區域的指標。shape 和 strides 屬性各自傳回一個 ctypes 整數陣列(如果陣列為 0 維,則傳回 None,表示 NULL 指標)。陣列的基礎 ctype 是與平台上指標大小相同的 ctype 整數。還有方法 data_as({ctype})
、shape_as(<base ctype>)
和 strides_as(<base ctype>)
。這些方法將資料作為您選擇的 ctype 物件傳回,並使用您選擇的底層基礎類型傳回形狀/步幅陣列。為了方便起見,ctypeslib
模組還包含 c_intp
作為 ctypes 整數資料類型,其大小與平台上 c_void_p
的大小相同(如果未安裝 ctypes,則其值為 None)。
呼叫函數#
函數作為載入的共享函式庫的屬性或項目存取。因此,如果 ./mylib.so
有一個名為 cool_function1
的函數,則可以將其存取為
lib = numpy.ctypeslib.load_library('mylib','.')
func1 = lib.cool_function1 # or equivalently
func1 = lib['cool_function1']
在 ctypes 中,函數的傳回值預設設定為 'int'。可以透過設定函數的 restype 屬性來更改此行為。如果函數沒有傳回值 ('void'),則對 restype 使用 None
func1.restype = None
如前所述,您也可以設定函數的 argtypes 屬性,以便讓 ctypes 在呼叫函數時檢查輸入引數的類型。使用 ndpointer
工廠函數來產生一個現成的類別,用於對新函數進行資料類型、形狀和旗標檢查。ndpointer
函數具有以下簽名
- ndpointer(dtype=None, ndim=None, shape=None, flags=None)#
值為
None
的關鍵字引數未經檢查。指定關鍵字會強制在轉換為 ctypes 相容物件時檢查 ndarray 的該方面。dtype 關鍵字可以是任何被理解為資料類型物件的物件。ndim 關鍵字應為整數,shape 關鍵字應為整數或整數序列。flags 關鍵字指定任何傳遞的陣列上所需的最小旗標。這可以指定為逗號分隔的要求字串、表示要求位元 OR 在一起的整數,或從具有必要要求的陣列的 flags 屬性傳回的旗標物件。
在 argtypes 方法中使用 ndpointer 類別可以顯著更安全地使用 ctypes 和 ndarray 的資料區域呼叫 C 函數。您可能仍然希望將該函數封裝在額外的 Python 封裝器中,使其更方便使用者使用(隱藏一些顯而易見的引數並使一些引數成為輸出引數)。在此過程中,NumPy 中的 requires
函數可能有用,可以從給定的輸入傳回正確類型的陣列。
完整範例#
在此範例中,我們將示範如何使用 ctypes 實作先前使用其他方法實作的加法函數和濾波函數。首先,實作演算法的 C 程式碼包含 zadd
、dadd
、sadd
、cadd
和 dfilter2d
函數。其中 zadd
函數是
/* Add arrays of contiguous data */
typedef struct {double real; double imag;} cdouble;
typedef struct {float real; float imag;} cfloat;
void zadd(cdouble *a, cdouble *b, cdouble *c, long n)
{
while (n--) {
c->real = a->real + b->real;
c->imag = a->imag + b->imag;
a++; b++; c++;
}
}
cadd
、dadd
和 sadd
也有類似的程式碼,分別處理複數浮點數、雙精度浮點數和浮點數資料類型
void cadd(cfloat *a, cfloat *b, cfloat *c, long n)
{
while (n--) {
c->real = a->real + b->real;
c->imag = a->imag + b->imag;
a++; b++; c++;
}
}
void dadd(double *a, double *b, double *c, long n)
{
while (n--) {
*c++ = *a++ + *b++;
}
}
void sadd(float *a, float *b, float *c, long n)
{
while (n--) {
*c++ = *a++ + *b++;
}
}
code.c
檔案也包含 dfilter2d
函數
/*
* Assumes b is contiguous and has strides that are multiples of
* sizeof(double)
*/
void
dfilter2d(double *a, double *b, ssize_t *astrides, ssize_t *dims)
{
ssize_t i, j, M, N, S0, S1;
ssize_t r, c, rm1, rp1, cp1, cm1;
M = dims[0]; N = dims[1];
S0 = astrides[0]/sizeof(double);
S1 = astrides[1]/sizeof(double);
for (i = 1; i < M - 1; i++) {
r = i*S0;
rp1 = r + S0;
rm1 = r - S0;
for (j = 1; j < N - 1; j++) {
c = j*S1;
cp1 = j + S1;
cm1 = j - S1;
b[i*N + j] = a[r + c] +
(a[rp1 + c] + a[rm1 + c] +
a[r + cp1] + a[r + cm1])*0.5 +
(a[rp1 + cp1] + a[rp1 + cm1] +
a[rm1 + cp1] + a[rm1 + cp1])*0.25;
}
}
}
此程式碼相較於 Fortran 等效程式碼的一個可能優勢是,它可以接受任意步幅(即非連續陣列),並且根據編譯器的最佳化能力,執行速度也可能更快。但是,它顯然比 filter.f
中的簡單程式碼更複雜。此程式碼必須編譯成共享函式庫。在我的 Linux 系統上,這是通過以下方式完成的
gcc -o code.so -shared code.c
這會在目前目錄中建立一個名為 code.so 的共享函式庫。在 Windows 上,別忘了在每個函數定義前的 void 行前面加上 __declspec(dllexport)
,或者編寫一個 code.def
檔案,列出要匯出的函數名稱。
應該為此共享函式庫建構一個合適的 Python 介面。為此,建立一個名為 interface.py 的檔案,並在頂部包含以下幾行
__all__ = ['add', 'filter2d']
import numpy as np
import os
_path = os.path.dirname('__file__')
lib = np.ctypeslib.load_library('code', _path)
_typedict = {'zadd' : complex, 'sadd' : np.single,
'cadd' : np.csingle, 'dadd' : float}
for name in _typedict.keys():
val = getattr(lib, name)
val.restype = None
_type = _typedict[name]
val.argtypes = [np.ctypeslib.ndpointer(_type,
flags='aligned, contiguous'),
np.ctypeslib.ndpointer(_type,
flags='aligned, contiguous'),
np.ctypeslib.ndpointer(_type,
flags='aligned, contiguous,'\
'writeable'),
np.ctypeslib.c_intp]
此程式碼載入名為 code.{ext}
的共享函式庫,該函式庫與此檔案位於同一路徑。然後,它為函式庫中包含的函數新增 void 返回類型。它還為函式庫中的函數新增了引數檢查,以便可以将 ndarray 作為前三個引數傳遞,並將整數(足夠大以容納平台上的指標)作為第四個引數。
設定濾波函數是類似的,並允許使用 ndarray 引數作為前兩個引數調用濾波函數,並使用指向整數的指標(足夠大以處理 ndarray 的步幅和形狀)作為最後兩個引數。
lib.dfilter2d.restype=None
lib.dfilter2d.argtypes = [np.ctypeslib.ndpointer(float, ndim=2,
flags='aligned'),
np.ctypeslib.ndpointer(float, ndim=2,
flags='aligned, contiguous,'\
'writeable'),
ctypes.POINTER(np.ctypeslib.c_intp),
ctypes.POINTER(np.ctypeslib.c_intp)]
接下來,定義一個簡單的選擇函數,該函數根據資料類型選擇要在共享函式庫中調用的加法函數
def select(dtype):
if dtype.char in ['?bBhHf']:
return lib.sadd, single
elif dtype.char in ['F']:
return lib.cadd, csingle
elif dtype.char in ['DG']:
return lib.zadd, complex
else:
return lib.dadd, float
return func, ntype
最後,介面要匯出的兩個函數可以簡單地寫成
def add(a, b):
requires = ['CONTIGUOUS', 'ALIGNED']
a = np.asanyarray(a)
func, dtype = select(a.dtype)
a = np.require(a, dtype, requires)
b = np.require(b, dtype, requires)
c = np.empty_like(a)
func(a,b,c,a.size)
return c
以及
def filter2d(a):
a = np.require(a, float, ['ALIGNED'])
b = np.zeros_like(a)
lib.dfilter2d(a, b, a.ctypes.strides, a.ctypes.shape)
return b
結論#
使用 ctypes 是將 Python 與任意 C 程式碼連接的強大方法。它擴展 Python 的優點包括
C 程式碼與 Python 程式碼的清晰分離
除了 Python 和 C 之外,無需學習新的語法
允許重複使用 C 程式碼
可以通過簡單的 Python 包裝器和搜尋函式庫來獲得為其他目的編寫的共享函式庫中的功能。
通過 ctypes 屬性輕鬆與 NumPy 集成
使用 ndpointer 類別工廠進行完整的引數檢查
其缺點包括
由於 distutils 缺乏對建構共享函式庫的支援,因此很難發布使用 ctypes 製作的擴充模組。
您必須擁有程式碼的共享函式庫(沒有靜態函式庫)。
對 C++ 程式碼及其不同的函式庫調用約定支援很少。您可能需要 C++ 程式碼周圍的 C 包裝器才能與 ctypes 一起使用(或者直接改用 Boost.Python)。
由於發布使用 ctypes 製作的擴充模組存在困難,因此 f2py 和 Cython 仍然是擴展 Python 以創建套件的最簡單方法。但是,在某些情況下,ctypes 是一種有用的替代方案。這應該為 ctypes 帶來更多功能,從而消除使用 ctypes 擴展 Python 和發布擴充的難度。
您可能會發現有用的其他工具#
這些工具已被其他 Python 使用者發現很有用,因此在此處包含。之所以單獨討論它們,是因為它們要么是現在由 f2py、Cython 或 ctypes 處理的較舊方法 (SWIG, PyFort),要么是因為缺乏合理的文檔 (SIP, Boost)。由於可以使用 Google 或其他搜尋引擎找到最相關的鏈接,並且此處提供的任何鏈接很快就會過時,因此未包含指向這些方法的鏈接。不要認為包含在此列表中意味著該套件值得關注。此處收集有關這些套件的信息是因為許多人發現它們很有用,並且我們希望為您提供盡可能多的選項來解決輕鬆整合程式碼的問題。
SWIG#
簡化封裝器和介面產生器 (SWIG) 是一種古老且相當穩定的方法,用於將 C/C++ 函式庫封裝到各種其他語言。它不專門理解 NumPy 陣列,但可以通過使用類型映射 (typemaps) 使其與 NumPy 一起使用。在 numpy.i 下的 numpy/tools/swig 目錄中以及一個使用它們的範例模組中,有一些範例類型映射。SWIG 擅長封裝大型 C/C++ 函式庫,因為它可以(幾乎)解析其標頭並自動產生介面。從技術上講,您需要產生一個 .i
檔案來定義介面。但是,通常,此 .i
檔案可以是標頭本身的一部分。介面通常需要進行一些調整才能非常有用。儘管已經出現了其他更針對 Python 的方法,但這種解析 C/C++ 標頭並自動產生介面的能力仍然使 SWIG 成為將 C/C++ 功能新增到 Python 中的有用方法。SWIG 實際上可以針對多種語言的擴展,但類型映射通常必須是特定於語言的。儘管如此,通過修改特定於 Python 的類型映射,SWIG 可以用於將函式庫與其他語言(例如 Perl、Tcl 和 Ruby)連接。
我使用 SWIG 的經驗總體上是積極的,因為它相對容易使用且功能強大。在更熟練地編寫 C 擴展之前,它經常被使用。但是,使用 SWIG 編寫自定義介面通常很麻煩,因為它必須使用類型映射的概念來完成,而類型映射不是 Python 特定的,並且以類似 C 的語法編寫。因此,其他粘合策略是首選,SWIG 可能僅被考慮用於封裝非常大的 C/C++ 函式庫。儘管如此,還有其他人非常樂意使用 SWIG。
SIP#
SIP 是另一個用於封裝 C/C++ 函式庫的工具,它是 Python 特定的,並且似乎對 C++ 有很好的支援。Riverbank Computing 開發了 SIP,以便為 QT 函式庫創建 Python 綁定。必須編寫介面檔案才能產生綁定,但介面檔案看起來很像 C/C++ 標頭檔案。雖然 SIP 不是完整的 C++ 解析器,但它理解相當多的 C++ 語法以及它自己的特殊指令,這些指令允許修改 Python 綁定的完成方式。它還允許用戶定義 Python 類型和 C/C++ 結構和類別之間的映射。
Boost Python#
Boost 是一個 C++ 函式庫的儲存庫,而 Boost.Python 是其中一個函式庫,它為將 C++ 類別和函數綁定到 Python 提供了簡潔的介面。Boost.Python 方法的驚人之處在於它完全在純 C++ 中工作,而無需引入新的語法。許多 C++ 用戶報告說,Boost.Python 使以無縫方式結合兩個世界的優點成為可能。使用 Boost 來封裝簡單的 C 子例程通常是過度的。它的主要目的是使 C++ 類別在 Python 中可用。因此,如果您有一組需要乾淨地整合到 Python 中的 C++ 類別,請考慮學習和使用 Boost.Python。
Pyfort#
Pyfort 是一個很好的工具,用於將 Fortran 和類似 Fortran 的 C 程式碼封裝到 Python 中,並支援 Numeric 陣列。它由傑出的電腦科學家 Paul Dubois 編寫,他是 Numeric(現已退休)的第一位維護者。值得一提的是,希望有人會更新 PyFort 以使其也適用於 NumPy 陣列,NumPy 陣列現在支援 Fortran 或 C 風格的連續陣列。