在這篇短文中,我們將詳細聊一聊如何用 C 或者 C++ 寫一個 Python 模組(或軟體包),內容主要參考 Python 官方檔案。作為範例,我也將用 C 寫一個簡單的 Python 模組,完成一個簡單的數學計算: n!=n×(n-1)×(n-2)… 。
為了實現上面的標的,我們需要兩個檔案:一個 Python 程式碼 setup.py
,以及我們實際編寫的 C 語言程式碼 cmath.c
。
總的來說,我們將用 setup.py
把 C 語言寫的程式碼 cmath.c
構建成一個 Python 庫(這其中包括編譯程式碼、查詢 Python C 庫、連線等操作)。
那麼,讓我們開始吧!
作者:Matthias Bitzer
編譯:歐剃
來源:優達學城Udacity(ID:youdaxue)
原文:medium.com/@matthiasbit
01 原理
為了讓我們的程式/模組能在 Python 程式碼中被呼叫執行,模組需要和 Python 直譯器 CPython
進行必要的通訊。因此,我們需要 Python.h
頭檔案裡面的若干物件,並用它們構建出合適的結構體。
基本上,我們要做的是把實際的 C 語言方法包裝起來,以便能夠被 Python 直譯器所呼叫,這樣我們的 Python 程式碼才能夠像使用普通的 Python 函式一樣,呼叫這個方法。
02 編寫演演算法並包裝
首先,我們要在 cmath.c
裡引入頭檔案:
#include Python.h
在 Python 頭檔案裡,我們需要用來和 Python 直譯器對接的物件(以及函式),都以 Py
開頭。在這裡,能代表所有 python 物件的 C 物件(基本上就是一個opaque
——“不透明”物件)叫做 PyObject
。
不過,在實際使用這些物件之前,我們先把求階乘的演演算法寫出來(註意,0的階乘是1):
int fastfactorial(int n){
if(n<=1)
return 1;
else
return n * fastfactorial(n-1);
}
接著,我們給這個函式進行一下包裝。這個包裹函式接收一個 PyObject 型別的指標(指向今後從 Python 程式碼傳入的引數)作為引數,再傳回一個 PyObject 型別的指標(指向上面函式的傳回值)給外部。
為此,我們用以下程式碼來實現這個包裹函式:
static PyObject* factorial(PyObject* self, PyObject* args){
int n;
if (!PyArg_ParseTuple(args,"i",&n;))
return NULL;
int result = fastfactorial(n);
return Py_BuildValue("i",result);
}
這個函式始終需要一個指向模組物件本身的 self
指標,以及一個指向從 Python 程式碼傳入引數的 args
指標(二者都是 PyObject
型別的物件)。我們用 PyArg_ParseTuple
方法來處理這些引數,並且宣告我們需要的是整數型別(第二個引數 "i"
),最後將處理結果賦值到變數 n
中。
接著自然是呼叫 fastfactorial(n)
來計算階乘,並用 Python 頭檔案裡的 Py_BuildValue
方法把傳回值塞回 PyObject*
型別裡。最後,我們的包裹函式將指向結果的指標物件傳回給外部。
03 組裝模組結構
現在,我們已經把實際的階乘函式封裝完畢,接下來需要構造一個 PyModuleDef
結構體的實體(這個物件也是由 Python.h
所定義的。這個結構體定義了模組的結構,以便 Python 直譯器載入呼叫。
而模組的另一個組成部分是定義它的所有方法,這由另一個結構體 PyMethodDef
實現——它其實就相當於一個陣列,裡面列出了模組中所有的方法和對應的說明。
在當前例子中,我們定義瞭如下的 PyMethodDef
物件:
static PyMethodDef mainMethods[] = {
{"factorial",factorial,METH_VARARGS,"Calculate the factorial of n"},
{NULL,NULL,0,NULL}
};
這個物件裡目前共有 2 個元素——我們在最末尾加入了一個由 NULL
組成的結構體,做為結尾。第 0 個物件是我們定義的方法,它的結構是:先是方法名 factorial
,其次是實際呼叫的函式物件,註意這裡呼叫的是上一節定義的包裹函式;接下來指定了這個方法是從 METH_VARARGS
這個常量中獲得它的引數;最後是一個說明字串。
於是,我們已經定義了這個 Python 模組中的所有方法(本例中就一個),我們可以建立一個 PyModuleDef
的實體,作為代表整個 Python 模組的物件。
程式碼如下:
static PyModuleDef cmath = {
PyModuleDef_HEAD_INIT,
"cmath","Factorial Calculation",
-1,
mainMethods
};
在上面的程式碼中,我們首先定義了模組名 cmath
以及簡短的檔案字串,然後再把所有的方法組成的陣列 mainMethods
放進去。
最後一步,我們要新增一個函式,並讓 python 程式碼匯入這個模組的時候執行這個函式。
程式碼如下:
PyMODINIT_FUNC PyInit_cmath(void){
return PyModule_Create(&cmath;);
}
函式的傳回型別是 PyMODINIT_FUNC
,這表明函式實際上傳回的是一個 PyObject
型別的指標。這個指標指向由 PyModule_Create
生成的 Python 模組本身(這個模組物件本身也是一個 PyObject
物件)。當一個模組被 Python 程式碼匯入時,這個方法就會被呼叫,並傳回一個指向整個模組物件,包含了所有方法的指標。
04 編譯打包模組
現在我們的 C 程式碼檔案已經準備好了,所有的方法都已經包裝到位,Python 直譯器匯入、執行所需的結構體也已經定義完善。於是,我們可以開始構建最終的二進位制檔案了。
在這個過程中,我們的 C 程式碼需要被編譯、並和正確的庫檔案連線(本例中,我們用到的主要是 Python
頭檔案中定義的那些方法和物件)。為了簡化構建過程,我們可以用到 distutils.core
模組裡的 setup
和 Extension
方法。
簡單地說,這兩個方法基本上能搞定整個構建過程。我們只要把 setup.py
和 cmath.c
放在同一個檔案夾裡,然後引入這兩個方法即可。
這是完整的 setup.py
檔案內容:
from distutils.core import setup, Extension
factorial_module = Extension('cmath',sources = ['cmath.c'])
setup(name = 'MathExtension',
version='1.0',
description = 'This is a math package',
ext_modules = [factorial_module]
)
在上面的程式碼中,我們首先宣告了 factorial_module
變數,作為一個 C 語言擴充套件物件,原始碼 source
來自我們的 C 程式碼檔案。這一行基本就是告訴 setup 方法要編譯的源檔案是哪個。
接下來,我們呼叫 setup()
函式,這個函式接收的引數就是將來要構建的包名(MathExtension
)、版本號(1.0)、簡短的描述檔案,以及要包括在內的 C 語言擴充套件/模組物件( factorial_module
)。這樣,setup.py
就寫好了,是不是很簡單?
最後,我們執行一下 setup.py
。執行時可以選擇兩種不同的樣式。如果是 build,程式就只編譯這個模組(一個 .so
格式的庫檔案)並把編譯結果放在當前檔案夾裡的 build 子檔案夾內;如果是 install
,則會將編譯結果放在 python 的環境變數 PATH 指向的檔案夾裡,以便其他程式呼叫。
今天的例子裡,我們選擇 build 選項。在終端/命令提示符裡輸入以下命令:
python setup.py build
如果一切正常,你就會在當前檔案夾裡看到一個 build
檔案夾,併在裡面看到編譯出來的 .so
檔案。這個庫檔案可以被 Python 指令碼呼叫,並執行我們用 C 編寫的階乘函式。
05 測試結果
讓我們試一下吧。我簡單地寫了一個 test.py
,並把它放在和 .so
檔案同一個檔案夾下,方便呼叫(當然,你如果用了 install 選項,那就無需這麼做,在任意目錄都能呼叫這個包)。
test.py
檔案的內容如下:
from cmath import factorial
print(factorial(6))
執行一下,得到結果 720。搞定!我們用 C 語言寫的這個小模組成功地匯入並執行啦!
恭喜你已經看完了今天的小教程,你打算給自己的 python 增加哪些威力強大的模組呢?
朋友會在“發現-看一看”看到你“在看”的內容