使用Python作為粘合劑

很多人都喜歡說Python是一種很棒的粘合語言。希望本章能說服你這是真的。Python的第一批科學家通常使用它來粘合在超級計算機上運行的大型應用程序代碼。在Python中編寫代碼比在shell腳本或Perl中編寫代碼更好,此外,輕松擴展Python的能力使得創(chuàng)建專門適應所解決問題的新類和類型變得相對容易。從這些早期貢獻者的交互中,Numeric出現(xiàn)了一個類似于數(shù)組的對象,可用于在這些應用程序之間傳遞數(shù)據(jù)。

隨著Numeric的成熟和發(fā)展成為NumPy,人們已經能夠在NumPy中直接編寫更多代碼。通常,此代碼對于生產使用來說足夠快,但仍有時需要訪問已編譯的代碼。要么從算法中獲得最后一點效率,要么更容易訪問用C / C ++或Fortran編寫的廣泛可用的代碼。

本章將回顧許多可用于訪問以其他編譯語言編寫的代碼的工具。有許多資源可供學習從Python調用其他編譯庫,本章的目的不是讓你成為專家。主要目標是讓您了解一些可能性,以便您知道“Google”的內容,以便了解更多信息。

從Python調用其他編譯庫

雖然Python是一種很好的語言并且很樂意編寫代碼,但它的動態(tài)特性會導致開銷,從而導致一些代碼(
for循環(huán)中的原始計算)比用靜態(tài)編譯語言編寫的等效代碼慢10-100倍。此外,由于在計算過程中創(chuàng)建和銷毀臨時數(shù)組,因此可能導致內存使用量大于必要值。對于許多類型的計算需求,通常不能節(jié)省額外的速度和內存消耗(至少對于代碼的時間或內存關鍵部分而言)。因此,最常見的需求之一是從Python代碼調用快速的機器代碼例程(例如使用C / C ++或Fortran編譯)。這相對容易做的事實是Python成為科學和工程編程的優(yōu)秀高級語言的一個重要原因。

它們是調用編譯代碼的兩種基本方法:編寫擴展模塊,然后使用import命令將其導入Python,或者使用ctypes


模塊直接從Python調用共享庫子例程。編寫擴展模塊是最常用的方法。

警告

如果你不小心,從Python調用C代碼會導致Python崩潰。本章中沒有一種方法可以免疫。您必須了解NumPy和正在使用的第三方庫處理數(shù)據(jù)的方式。

手工生成的包裝器

編寫擴展模塊中討論了擴展模塊。與編譯代碼接口的最基本方法是編寫擴展模塊并構造調用編譯代碼的模塊方法。為了提高可讀性,您的方法應該利用 PyArg_ParseTuple 調用在Python對象和C數(shù)據(jù)類型之間進行轉換。對于標準的C數(shù)據(jù)類型,可能已經有一個內置的轉換器。對于其他人,您可能需要編寫自己的轉換器并使用"O&"格式字符串,該字符串允許您指定一個函數(shù),該函數(shù)將用于執(zhí)行從Python對象到所需的任何C結構的轉換。

一旦執(zhí)行了對適當?shù)腃結構和C數(shù)據(jù)類型的轉換,包裝器中的下一步就是調用底層函數(shù)。如果底層函數(shù)是C或C ++,這很簡單。但是,為了調用Fortran代碼,您必須熟悉如何使用編譯器和平臺從C / C ++調用Fortran子例程。這可能會有所不同的平臺和編譯器(這是f2py使接口Fortran代碼的生活變得簡單的另一個原因)但通常涉及下劃線修改名稱以及所有變量都通過引用傳遞的事實(即所有參數(shù)都是指針)。

手工生成的包裝器的優(yōu)點是您可以完全控制C庫的使用和調用方式,從而可以實現(xiàn)精簡且緊湊的界面,并且只需最少的開銷。缺點是您必須編寫,調試和維護C代碼,盡管大多數(shù)代碼都可以使用其他擴展模塊中“剪切粘貼和修改”這種歷史悠久的技術進行調整。因為,調用額外的C代碼的過程是相當規(guī)范的,所以已經開發(fā)了代碼生成過程以使這個過程更容易。其中一種代碼生成技術與NumPy一起分發(fā),可以輕松地與Fortran和(簡單)C代碼集成。這個軟件包f2py將在下一節(jié)中簡要介紹。

f2py

F2py允許您自動構建一個擴展模塊,該模塊與Fortran 77/90/95代碼中的例程相連。它能夠解析Fortran 77/90/95代碼并自動為它遇到的子程序生成Python簽名,或者你可以通過構造一個接口定義文件(或修改f2py生成的文件)來指導子程序如何與Python接口。 )。

創(chuàng)建基本擴展模塊的源

引入f2py最簡單的方法可能是提供一個簡單的例子。這是一個名為的文件中包含的子程序之一
add.f

C
      SUBROUTINE ZADD(A,B,C,N)
C
      DOUBLE COMPLEX A(*)
      DOUBLE COMPLEX B(*)
      DOUBLE COMPLEX C(*)
      INTEGER N
      DO 20 J = 1, N
         C(J) = A(J)+B(J)
 20   CONTINUE
      END

此例程只是將元素添加到兩個連續(xù)的數(shù)組中,并將結果放在第三個數(shù)組中。所有三個數(shù)組的內存必須由調用例程提供。f2py可以自動生成此例程的一個非?;镜慕涌冢?/p>

f2py -m add add.f

假設您的搜索路徑設置正確,您應該能夠運行此命令。此命令將在當前目錄中生成名為addmodule.c的擴展模塊?,F(xiàn)在可以像使用任何其他擴展模塊一樣從Python編譯和使用此擴展模塊。

創(chuàng)建編譯的擴展模塊

您還可以獲取f2py來編譯add.f并編譯其生成的擴展模塊,只留下可以從Python導入的共享庫擴展文件:

f2py -c -m add add.f

此命令在當前目錄中留下名為add。{ext}的文件(其中{ext}是平臺上python擴展模塊的相應擴展名 - 所以,pyd )。然后可以從Python導入該模塊。它將包含添加的每個子例程的方法(zadd,cadd,dadd,sadd)。每個方法的docstring包含有關如何調用模塊方法的信息:

>>> import add
>>> print add.zadd.__doc__
zadd - Function signature:
  zadd(a,b,c,n)
Required arguments:
  a : input rank-1 array('D') with bounds (*)
  b : input rank-1 array('D') with bounds (*)
  c : input rank-1 array('D') with bounds (*)
  n : input int

改善基本界面

默認界面是fortran代碼到Python的非常直譯。Fortran數(shù)組參數(shù)現(xiàn)在必須是NumPy數(shù)組,整數(shù)參數(shù)應該是整數(shù)。接口將嘗試將所有參數(shù)轉換為其所需類型(和形狀),如果不成功則發(fā)出錯誤。但是,因為它對參數(shù)的語義一無所知(因此C是輸出而n應該與數(shù)組大小完全匹配),所以可能會以導致Python崩潰的方式濫用此函數(shù)。例如:

>>> add.zadd([1,2,3], [1,2], [3,4], 1000)

將導致程序在大多數(shù)系統(tǒng)上崩潰。在封面下,列表被轉換為正確的數(shù)組,但隨后底層的添加循環(huán)被告知超出分配的內存的邊界。

為了改進界面,應提供指令。這是通過構造接口定義文件來完成的。通常最好從f2py可以生成的接口文件開始(從中獲取其默認行為)。要獲取f2py以生成接口文件,請使用-h選項:

f2py -h add.pyf -m add add.f

此命令將文件add.pyf保留在當前目錄中。與zadd對應的此文件部分為:

subroutine zadd(a,b,c,n) ! in :add:add.f
   double complex dimension(*) :: a
   double complex dimension(*) :: b
   double complex dimension(*) :: c
   integer :: n
end subroutine zadd

通過放置intent指令和檢查代碼,可以清理接口,直到Python模塊方法更易于使用且更健壯。

subroutine zadd(a,b,c,n) ! in :add:add.f
   double complex dimension(n) :: a
   double complex dimension(n) :: b
   double complex intent(out),dimension(n) :: c
   integer intent(hide),depend(a) :: n=len(a)
end subroutine zadd

intent指令,intent(out)用于告訴c作為輸出變量的f2py,并且應該在傳遞給底層代碼之前由接口創(chuàng)建。intent(hide)指令告訴f2py不允許用戶指定變量n,而是從大小中獲取它a。depend(a)指令必須告訴f2py n的值取決于輸入a(因此在創(chuàng)建變量a之前它不會嘗試創(chuàng)建變量n)。

修改后add.pyf,可以通過編譯add.f95和生成新的python模塊文件add.pyf

f2py -c add.pyf add.f95

新界面有docstring:

>>> import add
>>> print add.zadd.__doc__
zadd - Function signature:
  c = zadd(a,b)
Required arguments:
  a : input rank-1 array('D') with bounds (n)
  b : input rank-1 array('D') with bounds (n)
Return objects:
  c : rank-1 array('D') with bounds (n)

現(xiàn)在,可以以更加健壯的方式調用該函數(shù):

>>> add.zadd([1,2,3],[4,5,6])
array([ 5.+0.j,  7.+0.j,  9.+0.j])

請注意自動轉換為正確的格式。

在Fortran源中插入指令

通過將變量指令作為特殊注釋放在原始fortran代碼中,也可以自動生成nice接口。因此,如果我修改源代碼包含:

C
      SUBROUTINE ZADD(A,B,C,N)
C
CF2PY INTENT(OUT) :: C
CF2PY INTENT(HIDE) :: N
CF2PY DOUBLE COMPLEX :: A(N)
CF2PY DOUBLE COMPLEX :: B(N)
CF2PY DOUBLE COMPLEX :: C(N)
      DOUBLE COMPLEX A(*)
      DOUBLE COMPLEX B(*)
      DOUBLE COMPLEX C(*)
      INTEGER N
      DO 20 J = 1, N
         C(J) = A(J) + B(J)
 20   CONTINUE
      END

然后,我可以使用以下命令編譯擴展模塊:

f2py -c -m add add.f

函數(shù)add.zadd的結果簽名與之前創(chuàng)建的簽名完全相同。如果原來的源代碼已經包含A(N),而不是A(*)等以BC,然后我可以得到(幾乎)相同的接口簡單地通過將
注釋行的源代碼。唯一的區(qū)別是,這是一個默認為長度的可選輸入。INTENT(OUT) :: CNA

過濾示例

用于與將要討論的其他方法進行比較。下面是使用固定平均濾波器過濾二維精度浮點數(shù)的二維數(shù)組的函數(shù)的另一個示例。從這個例子中可以清楚地看到使用Fortran索引到多維數(shù)組的優(yōu)勢。

SUBROUTINE DFILTER2D(A,B,M,N)
C
      DOUBLE PRECISION A(M,N)
      DOUBLE PRECISION B(M,N)
      INTEGER N, M
CF2PY INTENT(OUT) :: B
CF2PY INTENT(HIDE) :: N
CF2PY INTENT(HIDE) :: M
      DO 20 I = 2,M-1
         DO 40 J=2,N-1
            B(I,J) = A(I,J) +
     $           (A(I-1,J)+A(I+1,J) +
     $            A(I,J-1)+A(I,J+1) )*0.5D0 +
     $           (A(I-1,J-1) + A(I-1,J+1) +
     $            A(I+1,J-1) + A(I+1,J+1))*0.25D0
 40      CONTINUE
 20   CONTINUE
      END

此代碼可以編譯并鏈接到名為filter的擴展模塊中,使用:

f2py -c -m filter filter.f

這將在當前目錄中生成一個名為filter.so的擴展模塊,其中包含一個名為dfilter2d的方法,該方法返回輸入的過濾版本。

從Python中調用f2py

f2py程序是用Python編寫的,可以在代碼中運行,以便在運行時編譯Fortran代碼,如下所示:

from numpy import f2py
with open("add.f") as sourcefile:
    sourcecode = sourcefile.read()
f2py.compile(sourcecode, modulename='add')
import add

源字符串可以是任何有效的Fortran代碼。如果要保存擴展模塊源代碼,則source_fn關鍵字可以為編譯函數(shù)提供合適的文件名。

自動擴展模塊生成






如果要分發(fā)f2py擴展模塊,則只需要包含.pyf文件和Fortran代碼。NumPy中的distutils擴展允許您完全根據(jù)此接口文件定義擴展模塊。setup.py允許分發(fā)add.f模塊的有效文件(作為包的一部分,
f2py_examples以便將其加載為f2py_examples.add):

def configuration(parent_package='', top_path=None)
    from numpy.distutils.misc_util import Configuration
    config = Configuration('f2py_examples',parent_package, top_path)
    config.add_extension('add', sources=['add.pyf','add.f'])
    return config

if __name__ == '__main__':
    from numpy.distutils.core import setup
    setup(**configuration(top_path='').todict())

安裝新包裝很容易使用:

pip install .

假設您具有寫入主要site-packages目錄的正確權限,以獲取您正在使用的Python版本。要使生成的包工作,您需要創(chuàng)建一個名為__init__.py
(與目錄相同add.pyf)的文件。請注意,擴展模塊完全根據(jù)add.pyfadd.f文件定義。.pyf文件到.c文件的轉換由 numpy.disutils 處理。

結論

接口定義文件(.pyf)是如何微調Python和Fortran之間的接口的。在numpy / f2py / docs目錄中找到了適合f2py的文檔,其中NumPy安裝在你的系統(tǒng)上(通常在site-packages下)。有關使用f2py(包括如何使用它來包裝C代碼)的更多信息,請參見https://scipy-cookbook.readthedocs.io

“與其他語言接口”標題下的信息。

連接編譯代碼的f2py方法是目前最復雜和最集成的方法。它允許使用已編譯的代碼清晰地分離Python,同時仍允許單獨分發(fā)擴展模塊。唯一的缺點是它需要Fortran編譯器的存在才能讓用戶安裝代碼。然而,隨著免費編譯器g77,gfortran和g95以及高質量商業(yè)編譯器的存在,這種限制并不是特別繁重。在我看來,F(xiàn)ortran仍然是編寫快速而清晰的科學計算代碼的最簡單方法。它以最直接的方式處理復雜的數(shù)字和多維索引。但請注意,某些Fortran編譯器無法優(yōu)化代碼以及良好的手寫C代碼。

用Cython

Cython

是Python方言的編譯器,它為速度添加(可選)靜態(tài)類型,并允許將C或C ++代碼混合到模塊中。它生成C或C ++擴展,可以在Python代碼中編譯和導入。

如果您正在編寫一個擴展模塊,其中包含相當多的自己的算法代碼,那么Cython是一個很好的匹配。其功能之一是能夠輕松快速地處理多維數(shù)組。

請注意,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數(shù)組時才需要添加NumPy包含目錄(我們假設您使用的是Cython)。NumPy中的distutils擴展還包括支持自動生成擴展模塊并將其從.pyx文件鏈接。它的工作原理是,如果用戶沒有安裝Cython,那么它會查找具有相同文件名但.c擴展名的文件,然后使用該.c文件而不是嘗試再次生成文件。

如果你只是使用Cython來編譯一個標準的Python模塊,那么你將得到一個C擴展模塊,它通常比同等的Python模塊運行得快一點。通過使用cdef關鍵字靜態(tài)定義C變量,可以進一步提高速度。

讓我們看一下我們之前看過的兩個例子,看看如何使用Cython實現(xiàn)它們。這些示例使用Cython 0.21.1編譯為擴展模塊。

Cython中的復雜添加

這是一個名為Cython的模塊的一部分,add.pyx它實現(xiàn)了我們之前使用f2py實現(xiàn)的復雜加法函數(shù):

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語句從numpy.pxdCython附帶的標頭加載定義??雌饋鞱umPy是兩次進口的; cimport只使NumPy C-API可用,而常規(guī)import在運行時導致Python樣式導入,并且可以調用熟悉的NumPy Python API。

該示例還演示了Cython的“類型化內存視圖”,它類似于C級別的NumPy數(shù)組,因為它們是形狀和跨步的數(shù)組,知道它們自己的范圍(不同于通過裸指針尋址的C數(shù)組)。語法表示具有任意步幅的雙精度的一維數(shù)組(向量)。一個連續(xù)的整數(shù)數(shù)組將是,而浮點矩陣將是。double complex[:]int[::1]float[:, :]

顯示的注釋是cython.boundscheck裝飾器,它基于每個函數(shù)打開或關閉內存視圖訪問的邊界檢查。我們可以使用它來進一步加速我們的代碼,但代價是安全性(或者在進入循環(huán)之前進行手動檢查)。

除了視圖語法之外,該函數(shù)可立即被Python程序員讀取。變量的靜態(tài)類型i是隱式的。我們也可以使用Cython的特殊NumPy數(shù)組語法代替視圖語法,但首選視圖語法。

Cython中的圖像過濾器

我們使用Fortran創(chuàng)建的二維示例與在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

這個2-d平均濾波器運行得很快,因為循環(huán)在C中,并且指針計算僅在需要時完成。如果將上面的代碼編譯為模塊image,則img可以使用以下代碼非常快速地過濾2-d圖像:

import image
out = image.filter(img)

關于代碼,有兩點需要注意:首先,不可能將內存視圖返回給Python。相反,out首先創(chuàng)建NumPy數(shù)組,然后使用b該數(shù)組的視圖進行計算。其次,視圖b是鍵入的。這意味著具有連續(xù)行的2-d數(shù)組,即C矩陣順序。明確指定順序可以加速某些算法,因為它們可以跳過步幅計算。double[:, ::1]

Cython結論

Cython是幾個科學Python庫的首選擴展機制,包括Scipy,Pandas,SAGE,scikit-image和scikit-learn,以及XML處理庫LXML。語言和編譯器維護良好。

使用Cython有幾個缺點:

  1. 在編寫自定義算法時,有時在包裝現(xiàn)有C庫時,需要熟悉C語言。特別是,當使用C內存管理(malloc和朋友)時,很容易引入內存泄漏。但是,只是編譯重命名的Python模塊.pyx
    已經可以加快速度,并且添加一些類型聲明可以在某些代碼中提供顯著的加速。
  2. 很容易在Python和C之間失去一個清晰的分離,這使得重用你的C代碼用于其他非Python相關項目變得更加困難。
  3. Cython生成的C代碼難以閱讀和修改(并且通常編譯有令人煩惱但無害的警告)。

Cython生成的擴展模塊的一大優(yōu)勢是它們易于分發(fā)??傊?,Cython是一個非常強大的工具,可以粘合C代碼或快速生成擴展模塊,不應該被忽視。它對于不能或不會編寫C或Fortran代碼的人特別有用。

ctypes

ctypes


是一個包含在stdlib中的Python擴展模塊,它允許您直接從Python調用共享庫中的任意函數(shù)。這種方法允許您直接從Python接口C代碼。這開辟了大量可供Python使用的庫。然而,缺點是編碼錯誤很容易導致丑陋的程序崩潰(就像C中可能發(fā)生的那樣),因為對參數(shù)進行的類型或邊界檢查很少。當數(shù)組數(shù)據(jù)作為指向原始內存位置的指針傳入時尤其如此。那么你應該負??責子程序不會訪問實際數(shù)組區(qū)域之外的內存。但,

因為ctypes方法將原始接口暴露給已編譯的代碼,所以它并不總是容忍用戶錯誤。強大地使用ctypes模塊通常需要額外的Python代碼層,以便檢查傳遞給底層子例程的對象的數(shù)據(jù)類型和數(shù)組邊界。這個額外的檢查層(更不用說從ctypes對象到ctypes本身執(zhí)行的C-data類型的轉換)將使接口比手寫的擴展模塊接口慢。但是,如果被調用的C例程正在執(zhí)行任何大量工作,則此開銷應該可以忽略不計。如果你是一個具有弱C技能的優(yōu)秀Python程序員,那么ctypes是一種為編譯代碼的(共享)庫編寫有用接口的簡單方法。

要使用ctypes,你必須

  1. 有一個共享的庫。
  2. 加載共享庫。
  3. 將python對象轉換為ctypes理解的參數(shù)。
  4. 使用ctypes參數(shù)從庫中調用函數(shù)。

加載共享庫

共享庫有幾個要求,可以與特定于平臺的ctypes一起使用。本指南假設您熟悉在系統(tǒng)上創(chuàng)建共享庫(或者只是為您提供共享庫)。要記住的項目是:

  • 必須以特殊方式編譯共享庫( 例如, 使用-shared帶有gcc 的標志)。
  • 在某些平臺( 例如 Windows)上,共享庫需要一個.def文件,該文件指定要導出的函數(shù)。例如,mylib.def文件可能包含:
LIBRARY mylib.dll
EXPORTS
cool_function1
cool_function2

或者,您可以__declspec(dllexport)在函數(shù)的C定義中使用存儲類說明符
,以避免需要此.def文件。

Python distutils中沒有標準的方法來以跨平臺的方式創(chuàng)建標準共享庫(擴展模塊是Python理解的“特殊”共享庫)。因此,在編寫本書時,ctypes的一大缺點是難以以跨平臺的方式分發(fā)使用ctypes的Python擴展并包含您自己的代碼,這些代碼應編譯為用戶系統(tǒng)上的共享庫。

加載共享庫

加載共享庫的一種簡單但強大的方法是獲取絕對路徑名并使用ctypes的cdll對象加載它:

lib = ctypes.cdll[<full_path_name>]

但是,在Windows上,訪問該cdll方法的屬性將按當前目錄或PATH中的名稱加載第一個DLL。加載絕對路徑名稱需要一點技巧才能進行跨平臺工作,因為共享庫的擴展會有所不同。有一個ctypes.util.find_library實用程序可以簡化查找?guī)旒虞d的過程,但它不是萬無一失的。更復雜的是,不同平臺具有共享庫使用的不同默認擴展名(例如.dll - Windows,.so - Linux,.dylib - Mac OS X)。如果您使用ctypes包裝需要在多個平臺上工作的代碼,則還必須考慮這一點。

NumPy提供稱為ctypeslib.load_library(名稱,路徑)的便利功能
。此函數(shù)采用共享庫的名稱(包括任何前綴,如'lib'但不包括擴展名)和共享庫所在的路徑。它返回一個ctypes庫對象,或者OSError如果找不到庫則引發(fā)一個或者ImportError如果ctypes模塊不可用則引發(fā)一個。(Windows用戶:使用加載的ctypes庫對象
load_library總是在假定cdecl調用約定的情況下加載。請參閱下面的ctypes文檔ctypes.windll和/或ctypes.oledll
了解在其他調用約定下加載庫的方法)。

共享庫中的函數(shù)可用作ctypes庫對象的屬性(從中返回ctypeslib.load_library)或使用lib['func_name']語法作為項目。如果函數(shù)名包含Python變量名中不允許的字符,則后一種檢索函數(shù)名的方法特別有用。

轉換參數(shù)

Python int / long,字符串和unicode對象會根據(jù)需要自動轉換為等效的ctypes參數(shù)None對象也會自動轉換為NULL指針。必須將所有其他Python對象轉換為特定于ctypes的類型。圍繞此限制有兩種方法允許ctypes與其他對象集成。

  1. 不要設置函數(shù)對象的argtypes屬性,并_as_parameter_為要傳入的對象定義
    方法。該
    _as_parameter_方法必須返回一個Python int,它將直接傳遞給函數(shù)。
  2. 將argtypes屬性設置為一個列表,其條目包含具有名為from_param的類方法的對象,該類方法知道如何將對象轉換為ctypes可以理解的對象(具有該_as_parameter_屬性的int / long,字符串,unicode或對象)。

NumPy使用兩種方法,優(yōu)先選擇第二種方法,因為它可以更安全。ndarray的ctypes屬性返回一個對象,該對象具有一個_as_parameter_返回整數(shù)的屬性,該整數(shù)表示與之關聯(lián)的ndarray的地址。因此,可以將此ctypes屬性對象直接傳遞給期望指向ndarray中數(shù)據(jù)的指針的函數(shù)。調用者必須確保ndarray對象具有正確的類型,形狀,并且設置了正確的標志,否則如果傳入指向不適當數(shù)組的數(shù)據(jù)指針則會導致令人討厭的崩潰。

為了實現(xiàn)第二種方法,NumPy ndpointernumpy.ctypeslib

模塊中提供了類工廠函數(shù)。此類工廠函數(shù)生成一個適當?shù)念?,可以放在ctypes函數(shù)的argtypes屬性條目中。該類將包含一個from_param方法,ctypes將使用該方法將傳入函數(shù)的任何ndarray轉換為ctypes識別的對象。在此過程中,轉換將執(zhí)行檢查用戶在調用中指定的ndarray的任何屬性ndpointer。可以檢查的ndarray的方面包括數(shù)據(jù)類型,維度的數(shù)量,形狀和/或傳遞的任何數(shù)組上的標志的狀態(tài)。from_param方法的返回值是數(shù)組的ctypes屬性(因為它包含_as_parameter_
ctypes可以直接使用指向數(shù)組數(shù)據(jù)區(qū)域的屬性。

ndarray的ctypes屬性還賦予了額外的屬性,這些屬性在將有關數(shù)組的其他信息傳遞給ctypes函數(shù)時可能很方便。屬性數(shù)據(jù)
形狀步幅可以提供與數(shù)據(jù)區(qū)域,形狀和數(shù)組步幅相對應的ctypes兼容類型。data屬性返回c_void_p表示指向數(shù)據(jù)區(qū)域的指針。shape和strides屬性各自返回一個ctypes整數(shù)數(shù)組(如果是0-d數(shù)組,則返回None表示NULL指針)。數(shù)組的基本ctype是與平臺上的指針大小相同的ctype整數(shù)。還有一些方法
data_as({ctype}),和shape_as()strides_as()。它們將數(shù)據(jù)作為您選擇的ctype對象返回,并使用您選擇的基礎類型返回shape / strides數(shù)組。為方便起見,該ctypeslib模塊還包含c_intp一個ctypes整數(shù)數(shù)據(jù)類型,其大小c_void_p``與平臺上的大小相同
(如果未安裝ctypes,則其值為None)。

調用函數(shù)

該函數(shù)作為加載的共享庫的屬性或項目進行訪問。因此,如果./mylib.so有一個名為的函數(shù)
cool_function1,我可以訪問此函數(shù):

lib = numpy.ctypeslib.load_library('mylib','.')
func1 = lib.cool_function1  # or equivalently
func1 = lib['cool_function1']

在ctypes中,函數(shù)的返回值默認設置為“int”??梢酝ㄟ^設置函數(shù)的restype屬性來更改此行為。如果函數(shù)沒有返回值('void'),則使用None作為restype:

func1.restype = None

如前所述,您還可以設置函數(shù)的argtypes屬性,以便在調用函數(shù)時讓ctypes檢查輸入?yún)?shù)的類型。使用ndpointer工廠函數(shù)生成現(xiàn)成的類,以便對新函數(shù)進行數(shù)據(jù)類型,形狀和標志檢查。該ndpointer功能具有簽名

ndpointerdtype = Nonendim = None , shape = Noneflags = None?

None不檢查具有該值的關鍵字參數(shù)。指定關鍵字會強制在轉換為與ctypes兼容的對象時檢查ndarray的該方面。dtype關鍵字可以是任何被理解為數(shù)據(jù)類型對象的對象。ndim關鍵字應為整數(shù),shape關鍵字應為整數(shù)或整數(shù)序列。flags關鍵字指定傳入的任何數(shù)組所需的最小標志。這可以指定為逗號分隔要求的字符串,指示需求位OR'd在一起的整數(shù),或者從flags的flags屬性返回的flags對象。具有必要要求的數(shù)組。

在argtypes方法中使用ndpointer類可以使用ctypes和ndarray的數(shù)據(jù)區(qū)調用C函數(shù)更加安全。您可能仍希望將該函數(shù)包裝在另一個Python包裝器中,以使其對用戶友好(隱藏一些明顯的參數(shù)并使一些參數(shù)輸出參數(shù))。在此過程中,requiresNumPy中的函數(shù)可能對從給定輸入返回正確類型的數(shù)組很有用。

完整的例子






在這個例子中,我將展示如何使用其他方法實現(xiàn)的加法函數(shù)和過濾函數(shù)可以使用ctypes實現(xiàn)。第一,它實現(xiàn)了算法的C代碼所包含的功能zadd,daddsadd,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用于處理復雜的浮點,雙精度和浮點數(shù)據(jù)類型,分別為:

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等效代碼的一個可能的優(yōu)點是它需要任意跨越(即非連續(xù)數(shù)組),并且還可能運行得更快,具體取決于編譯器的優(yōu)化功能。但是,它顯然比簡單的代碼更復雜filter.f。必須將此代碼編譯到共享庫中。在我的Linux系統(tǒng)上,這是使用以下方法完成

gcc -o code.so -shared code.c

這會在當前目錄中創(chuàng)建名為code.so的shared_library。在Windows上,不要忘記__declspec(dllexport)在每個函數(shù)定義之前的行上添加void,或者寫一個
code.def列出要導出的函數(shù)名稱的文件。

應構建適用于此共享庫的Python接口。為此,請在頂部創(chuàng)建一個名為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}位于與此文件相同的路徑中的共享庫。然后,它會向庫中包含的函數(shù)添加返回類型的void。它還將參數(shù)檢查添加到庫中的函數(shù),以便ndarrays可以作為前三個參數(shù)與一個整數(shù)(大到足以在平臺上保存指針)作為第四個參數(shù)傳遞。

設置過濾函數(shù)是類似的,并允許使用ndarray參數(shù)作為前兩個參數(shù)調用過濾函數(shù),并使用指向整數(shù)的指針(大到足以處理ndarray的步幅和形狀)作為最后兩個參數(shù):

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)]

接下來,定義一個簡單的選擇函數(shù),根據(jù)數(shù)據(jù)類型選擇在共享庫中調用哪個添加函數(shù):

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

最后,接口導出的兩個函數(shù)可以簡單地寫成:

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結論

使用ctypes是一種將Python與任意C代碼連接起來的強大方法。它擴展Python的優(yōu)點包括

  • 從Python代碼中清除C代碼的分離
    • 除了Python和C之外,無需學習新的語法
    • 允許重復使用C代碼
    • 可以使用簡單的Python包裝器獲取為其他目的編寫的共享庫中的功能并搜索庫。
  • 通過ctypes屬性輕松與NumPy集成
  • 使用ndpointer類工廠進行完整的參數(shù)檢查

它的缺點包括

  • 由于缺乏在distutils中構建共享庫的支持,很難分發(fā)使用ctypes創(chuàng)建的擴展模塊(但我懷疑這會隨著時間的推移而改變)。
  • 您必須擁有代碼的共享庫(沒有靜態(tài)庫)。
  • 很少支持C ++代碼及其不同的庫調用約定。你可能需要一個圍繞C ++代碼的C包裝器來與ctypes一起使用(或者只是使用Boost.Python)。

由于難以分發(fā)使用ctypes創(chuàng)建的擴展模塊,因此f2py和Cython仍然是擴展Python以創(chuàng)建包的最簡單方法。但是,在某些情況下,ctypes是一種有用的替代品。這應該為ctypes帶來更多功能,這將消除擴展Python和使用ctypes分發(fā)擴展的難度。

您可能會覺得有用的其他工具

使用Python的其他人發(fā)現(xiàn)這些工具很有用,因此包含在這里。它們是分開討論的,因為它們要么是現(xiàn)在由f2py,Cython或ctypes(SWIG,PyFort)處理的舊方法,要么是因為我對它們不太了解(SIP,Boost)。我沒有添加這些方法的鏈接,因為我的經驗是您可以使用Google或其他搜索引擎更快地找到最相關的鏈接,此處提供的任何鏈接都會很快過時。不要以為僅僅因為它包含在此列表中,我認為該軟件包不值得您關注。我包含了有關這些軟件包的信息,因為很多人發(fā)現(xiàn)它們很有用,我想盡可能多地為您提供解決易于集成代碼問題的選項。

SWIG

簡化的包裝器和接口生成器(SWIG)是一種古老且相當穩(wěn)定的方法,用于將C / C ++ - 庫包裝到各種其他語言中。它并不特別了解NumPy數(shù)組,但可以通過使用類型映射與NumPy一起使用。numpy.i下的numpy / tools / swig目錄中有一些示例類型映射以及一個使用它們的示例模塊。SWIG擅長包裝大型C / C ++庫,因為它可以(幾乎)解析其頭文件并自動生成一個接口。從技術上講,您需要生成.i
定義接口的文件。但是,這通常是這樣.ifile可以是標題本身的一部分。界面通常需要一些調整才能非常有用。這種解析C / C ++頭文件和自動生成界面的能力仍然使SWIG成為一種有用的方法,可以將C / C ++中的functionalilty添加到Python中,盡管已經出現(xiàn)了更多針對Python的其他方法。SWIG實際上可以定位多種語言的擴展,但是這些類型映射通常必須是特定于語言的。盡管如此,通過修改特定于Python的類型映射,SWIG可用于將庫與其他語言(如Perl,Tcl和Ruby)連接。

我對SWIG的體驗總體上是積極的,因為它相對容易使用且非常強大。在更熟練地編寫C擴展之前,我經常使用它。但是,我很難用SWIG編寫自定義接口,因為它必須使用非Python特定的類型映射的概念來完成,并且使用類似C的語法編寫。因此,我傾向于選擇其他粘合策略,并且只會嘗試使用SWIG來包裝一個非常大的C / C ++庫。盡管如此,還有其他人非常愉快地使用SWIG。

SIP

SIP是另一種用于包裝特定于Python的C / C ++庫的工具,似乎對C ++有很好的支持。Riverbank Computing開發(fā)了SIP,以便為QT庫創(chuàng)建Python綁定。必須編寫接口文件以生成綁定,但接口文件看起來很像C / C ++頭文件。雖然SIP不是一個完整的C ++解析器,但它理解了相當多的C ++語法以及它自己的特殊指令,這些指令允許修改Python綁定的完成方式。它還允許用戶定義Python類型和C / C ++結構和類之間的映射。

提升Python

Boost是C ++庫的存儲庫,Boost.Python是其中一個庫,它提供了一個簡潔的接口,用于將C ++類和函數(shù)綁定到Python。Boost.Python方法的神奇之處在于它完全在純C ++中工作而不引入新語法。許多C ++用戶報告稱,Boost.Python可以無縫地結合兩者的優(yōu)點。我沒有使用過Boost.Python,因為我不是C ++的大用戶,并且使用Boost來包裝簡單的C子例程通常都是過度殺戮。它的主要目的是使Python中的C ++類可用。因此,如果您有一組需要完全集成到Python中的C ++類,請考慮學習并使用Boost.Python。

PyFort

PyFort是一個很好的工具,可以將Fortran和類似Fortran的C代碼包裝到Python中,并支持數(shù)值數(shù)組。它由著名計算機科學家Paul Dubois編寫,是Numeric(現(xiàn)已退休)的第一個維護者。值得一提的是希望有人會更新PyFort以使用NumPy數(shù)組,現(xiàn)在支持Fortran或C風格的連續(xù)數(shù)組。

作者:柯廣的網絡日志 ? 使用Python作為粘合劑

微信公眾號:Java大數(shù)據(jù)與數(shù)據(jù)倉庫