Python - 擴充套件進階



使用任何編譯語言(如 C、C++ 或 Java)編寫的任何程式碼都可以整合或匯入到另一個 Python 指令碼中。此程式碼被視為“擴充套件”。

Python 擴充套件模組只不過是一個普通的 C 庫。在 Unix 機器上,這些庫通常以.so(共享物件)結尾。在 Windows 機器上,您通常會看到.dll(動態連結庫)。

編寫擴充套件的先決條件

要開始編寫擴充套件,您將需要 Python 標頭檔案。

  • 在 Unix 機器上,這通常需要安裝特定於開發人員的軟體包。

  • Windows 使用者在使用二進位制 Python 安裝程式時,會將這些標頭檔案作為軟體包的一部分獲得。

此外,假設您精通 C 或 C++,可以使用 C 程式設計編寫任何 Python 擴充套件。

首先了解 Python 擴充套件

要首次瞭解 Python 擴充套件模組,您需要將程式碼分成四個部分:

  • 標頭檔案Python.h

  • 您希望公開為模組介面的 C 函式。

  • 一個表,將 Python 開發人員看到的函式名稱對映到擴充套件模組內的 C 函式。

  • 一個初始化函式。

標頭檔案 Python.h

您需要在 C 原始檔中包含 Python.h 標頭檔案,這使您可以訪問用於將模組掛接到直譯器的內部 Python API。

確保在任何其他可能需要的標頭檔案之前包含 Python.h。您需要在包含之後跟著您想從 Python 中呼叫的函式。

C 函式

C 函式實現的簽名始終採用以下三種形式之一:

static PyObject *MyFunction(PyObject *self, PyObject *args);
static PyObject *MyFunctionWithKeywords(PyObject *self,
   PyObject *args,
   PyObject *kw);
static PyObject *MyFunctionWithNoArgs(PyObject *self);

前面每個宣告都返回一個 Python 物件。Python 中沒有像 C 中那樣的 void 函式。如果您不希望函式返回值,則返回 Python 的None值的 C 等價物。Python 標頭檔案定義了一個宏 Py_RETURN_NONE,它為我們執行此操作。

C 函式的名稱可以是您喜歡的任何名稱,因為它們永遠不會在擴充套件模組之外可見。它們被定義為靜態函式。

您的 C 函式通常透過組合 Python 模組和函式名稱來命名,如下所示:

static PyObject *module_func(PyObject *self, PyObject *args) {
   /* Do your stuff here. */
   Py_RETURN_NONE;
}

這是一個名為 func 的 Python 函式,位於模組 module 中。您將把指向 C 函式的指標放入模組的方法表中,該方法表通常位於原始碼的下一部分。

方法對映表

此方法表是 PyMethodDef 結構的簡單陣列。該結構看起來像這樣:

struct PyMethodDef {
   char *ml_name;
   PyCFunction ml_meth;
   int ml_flags;
   char *ml_doc;
};

以下是此結構成員的描述:

  • ml_name - 這是函式的名稱,當 Python 直譯器在 Python 程式中使用時,它會顯示該名稱。

  • ml_meth - 這是具有任何一個簽名的函式的地址,如上一節所述。

  • ml_flags - 這告訴直譯器 ml_meth 使用哪三個簽名。

    • 此標誌通常的值為 METH_VARARGS。

    • 如果要允許關鍵字引數進入函式,則可以將此標誌與 METH_KEYWORDS 進行按位或運算。

    • 這也可能具有 METH_NOARGS 的值,表示您不想接受任何引數。

  • mml_doc − 這是函式的文件字串,如果您不想編寫,可以為 NULL。

此表需要以哨兵結束,哨兵由相應成員的 NULL 和 0 值組成。

示例

對於上面定義的函式,我們有以下方法對映表:

static PyMethodDef module_methods[] = {
   { "func", (PyCFunction)module_func, METH_NOARGS, NULL },
   { NULL, NULL, 0, NULL }
};

初始化函式

擴充套件模組的最後一部分是初始化函式。當載入模組時,Python 直譯器會呼叫此函式。要求函式名為 initModule,其中 Module 是模組的名稱。

初始化函式需要從您將要構建的庫中匯出。Python 標頭檔案定義 PyMODINIT_FUNC 以包含適當的咒語,以便在我們要編譯的特定環境中發生這種情況。您所要做的就是在定義函式時使用它。

您的 C 初始化函式通常具有以下總體結構:

PyMODINIT_FUNC initModule() {
   Py_InitModule3(func, module_methods, "docstring...");
}

以下是 Py_InitModule3 函式的描述:

  • func − 這是要匯出的函式。

  • module_methods − 這是上面定義的對映表名稱。

  • docstring − 這是您想要在擴充套件中提供的註釋。

將所有這些放在一起,看起來如下所示:

#include <Python.h>
static PyObject *module_func(PyObject *self, PyObject *args) {
   /* Do your stuff here. */
   Py_RETURN_NONE;
}
static PyMethodDef module_methods[] = {
   { "func", (PyCFunction)module_func, METH_NOARGS, NULL },
   { NULL, NULL, 0, NULL }
};
PyMODINIT_FUNC initModule() {
   Py_InitModule3(func, module_methods, "docstring...");
}

示例

一個簡單的示例,它利用了上述所有概念:

#include <Python.h>
static PyObject* helloworld(PyObject* self)
{
   return Py_BuildValue("s", "Hello, Python extensions!!");
}
static char helloworld_docs[] =
   "helloworld( ): Any message you want to put here!!\n";
static PyMethodDef helloworld_funcs[] = {
   {"helloworld", (PyCFunction)helloworld,
   METH_NOARGS, helloworld_docs},
   {NULL}
};
void inithelloworld(void)
{
   Py_InitModule3("helloworld", helloworld_funcs,
      "Extension module example!");
}

這裡 Py_BuildValue 函式用於構建 Python 值。將上述程式碼儲存在 hello.c 檔案中。我們將瞭解如何編譯和安裝此模組以便從 Python 指令碼中呼叫。

構建和安裝擴充套件

distutils 包使以標準方式分發 Python 模組(純 Python 模組和擴充套件模組)變得非常容易。模組以原始碼形式分發,透過通常稱為 setup.pyas 的安裝指令碼進行構建和安裝。

對於上述模組,您需要準備以下 setup.py 指令碼:

from distutils.core import setup, Extension
setup(name='helloworld', version='1.0', \
   ext_modules=[Extension('helloworld', ['hello.c'])])

現在,使用以下命令,它將執行所有必要的編譯和連結步驟,使用正確的編譯器和連結器命令和標誌,並將生成的動態庫複製到適當的目錄中:

$ python setup.py install

在基於 Unix 的系統上,您很可能需要以 root 身份執行此命令才能有權寫入 site-packages 目錄。這在 Windows 上通常不是問題。

匯入擴充套件

安裝擴充套件後,您將能夠在 Python 指令碼中匯入和呼叫該擴充套件,如下所示:

import helloworld
print helloworld.helloworld()

這將產生以下輸出

Hello, Python extensions!!

傳遞函式引數

由於您很可能希望定義接受引數的函式,因此您可以為 C 函式使用其他簽名之一。例如,以下接受一些引數的函式將這樣定義:

static PyObject *module_func(PyObject *self, PyObject *args) {
   /* Parse args and do something interesting here. */
   Py_RETURN_NONE;
}

包含新函式條目的方法表將如下所示:

static PyMethodDef module_methods[] = {
   { "func", (PyCFunction)module_func, METH_NOARGS, NULL },
   { "func", module_func, METH_VARARGS, NULL },
   { NULL, NULL, 0, NULL }
};

您可以使用 API PyArg_ParseTuple 函式從傳遞到 C 函式的一個 PyObject 指標中提取引數。

PyArg_ParseTuple 的第一個引數是 args 引數。這是您將要解析的物件。第二個引數是格式字串,描述了您期望引數出現的格式。每個引數在格式字串中由一個或多個字元表示,如下所示。

static PyObject *module_func(PyObject *self, PyObject *args) {
   int i;
   double d;
   char *s;
   if (!PyArg_ParseTuple(args, "ids", &i, &d, &s)) {
      return NULL;
   }

   /* Do something interesting here. */
   Py_RETURN_NONE;
}

編譯模組的新版本並匯入它使您可以使用任意數量的任意型別的引數來呼叫新函式:

module.func(1, s="three", d=2.0)
module.func(i=1, d=2.0, s="three")
module.func(s="three", d=2.0, i=1)

您可能還可以想出更多變化。

PyArg_ParseTuple 函式

re 是PyArg_ParseTuple 函式的標準簽名:

int PyArg_ParseTuple(PyObject* tuple,char* format,...)

此函式對於錯誤返回 0,對於成功返回非 0 值。Tuple 是作為 C 函式第二個引數的 PyObject*。這裡的 format 是一個 C 字串,描述了必選和可選引數。

以下是PyArg_ParseTuple 函式的格式程式碼列表:

程式碼 C 型別 含義
c char 長度為 1 的 Python 字串變為 C char。
d double Python float 變為 C double。
f float Python float 變為 C float。
i int Python int 變為 C int。
l long Python int 變為 C long。
L long long Python int 變為 C long long。
O PyObject* 獲取對 Python 引數的非 NULL 借用引用。
S char* 沒有嵌入空字元的 Python 字串到 C char*。
s# char*+int 任何 Python 字串到 C 地址和長度。
t# char*+int 只讀單段緩衝區到 C 地址和長度。
u Py_UNICODE* 沒有嵌入空字元的 Python Unicode 到 C。
u# Py_UNICODE*+int 任何 Python Unicode C 地址和長度。
w# char*+int 讀/寫單段緩衝區到 C 地址和長度。
z char* 類似於 s,也接受 None(將 C char* 設定為 NULL)。
z# char*+int 類似於 s#,也接受 None(將 C char* 設定為 NULL)。
(...) 根據 ... Python 序列被視為每個專案一個引數。
| 以下引數是可選的。
: 格式結束,後跟錯誤訊息的函式名稱。
; 格式結束,後跟完整的錯誤訊息文字。

返回值

Py_BuildValue 使用與 PyArg_ParseTuple 非常相似的格式字串。您不是傳入要構建的值的地址,而是傳入實際的值。以下是一個示例,展示瞭如何實現 add 函式。

static PyObject *foo_add(PyObject *self, PyObject *args) {
   int a;
   int b;
   if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
      return NULL;
   }
   return Py_BuildValue("i", a + b);
}

如果用 Python 實現,它將如下所示:

def add(a, b):
   return (a + b)

您可以從函式中返回兩個值,如下所示。這將使用 Python 中的列表捕獲。

static PyObject *foo_add_subtract(PyObject *self, PyObject *args) {
   int a;
   int b;
   if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
      return NULL;
   }
   return Py_BuildValue("ii", a + b, a - b);
}

如果用 Python 實現,它將如下所示:

def add_subtract(a, b):
   return (a + b, a - b)

Py_BuildValue 函式

以下是Py_BuildValue 函式的標準簽名:

PyObject* Py_BuildValue(char* format,...)

這裡的 format 是一個 C 字串,描述了要構建的 Python 物件。Py_BuildValue 的以下引數是構建結果的 C 值。PyObject* 結果是一個新引用。

下表列出了常用的程式碼字串,其中零個或多個程式碼字串連線成字串格式。

程式碼 C 型別 含義
c char C char 變為長度為 1 的 Python 字串。
d double C double 變為 Python float。
f float C float 變為 Python float。
i int C int 變為 Python int
l long C long 變為 Python int
N PyObject* 傳遞 Python 物件並竊取引用。
O PyObject* 傳遞 Python 物件並按正常方式 INCREF。
O& convert+void* 任意轉換
s char* C 以 0 結尾的 char* 到 Python 字串,或 NULL 到 None。
s# char*+int C char* 和長度到 Python 字串,或 NULL 到 None。
u Py_UNICODE* C 寬的、以 null 結尾的字串到 Python Unicode,或 NULL 到 None。
u# Py_UNICODE*+int C 寬字串和長度到 Python Unicode,或 NULL 到 None。
w# char*+int 讀/寫單段緩衝區到 C 地址和長度。
z char* 類似於 s,也接受 None(將 C char* 設定為 NULL)。
z# char*+int 類似於 s#,也接受 None(將 C char* 設定為 NULL)。
(...) 根據 ... 從 C 值構建 Python 元組。
[...] 根據 ... 從 C 值構建 Python 列表。
{...} 根據 ... 從 C 值構建 Python 字典,交替鍵和值。

程式碼 {...} 從偶數個 C 值構建字典,交替鍵和值。例如,Py_BuildValue("{issi}",23,"zig","zag",42) 返回類似於 Python 的 {23:'zig','zag':42} 的字典

廣告