擴充/嵌入常見問題集

我可以在 C 中建立自己的函式嗎?

是的,你可以在 C 中建立包含函式、變數、例外甚至新型別的內建模組,擴充和嵌入 Python 直譯器 文件中有相關說明。

大多數中級或進階 Python 書籍也會涵蓋這個主題。

我可以在 C++ 中建立自己的函式嗎?

是的,可使用 C++ 中的 C 相容性功能。將 extern "C" { ... } 放在 Python 引入檔案周圍,並將 extern "C" 放在每個將由 Python 直譯器呼叫的函式之前。但具有構造函式的全域或靜態 C++ 物件可能不是一個好主意。

寫 C 很難;還有其他選擇嗎?

There are a number of alternatives to writing your own C extensions, depending on what you're trying to do. Recommended third party tools offer both simpler and more sophisticated approaches to creating C and C++ extensions for Python.

如何從 C 執行任意 Python 陳述式?

執行此操作的最高階函式是 PyRun_SimpleString(),它接受一個要在模組 __main__ 的情境中執行的單一字串引數,成功時回傳 0,發生例外(包括 SyntaxError)時回傳 -1。如果你想要更多控制,請使用 PyRun_String();請參閱 Python/pythonrun.cPyRun_SimpleString() 的原始碼。

如何計算來自 C 的任意 Python 運算式?

呼叫前一個問題中的 PyRun_String() 函式,並使用起始符號 Py_eval_input;它會剖析一個運算式,計算它並回傳它的值。

如何從 Python 物件中提取 C 值?

這取決於物件的型別。如果它是一個元組,PyTuple_Size() 會回傳它的長度,PyTuple_GetItem() 則回傳指定索引的項目。串列具有類似的函式 PyList_Size()PyList_GetItem()

對於位元組,PyBytes_Size() 會回傳它的長度,PyBytes_AsStringAndSize() 則提供指向該值與該長度的指標。請注意,Python 位元組物件可能包含空位元組,因此不應使用 C 的 strlen()

要測試物件的型別,首先確保它不是 NULL,然後再使用 PyBytes_Check()PyTuple_Check()PyList_Check() 等函式。

還有一個針對 Python 物件的高階 API,它由所謂的「抽象」介面所提供 —— 請閱讀 Include/abstract.h 以了解更多詳細資訊。它允許使用 PySequence_Length()PySequence_GetItem() 等函式的呼叫以及許多其他有用的協定,例如數值(PyNumber_Index() 等)和 PyMapping API 中的對映,來與任何類型的 Python 序列做介接。

如何使用 Py_BuildValue() 建立任意長度的元組?

這無法做到。請改用 PyTuple_Pack()

如何從 C 呼叫物件的方法?

PyObject_CallMethod() 函式可用於呼叫物件的任意方法。參數是物件、要呼叫的方法名稱、與 Py_BuildValue() 一起使用的格式字串,以及引數值:

PyObject *
PyObject_CallMethod(PyObject *object, const char *method_name,
                    const char *arg_format, ...);

這適用於任何具有方法的物件 —— 無論是內建的還是使用者定義的。你負責最終為回傳值來 Py_DECREF()

例如,使用引數 10、0 呼叫檔案物件的 "seek" 方法(假設檔案物件指標為 "f"):

res = PyObject_CallMethod(f, "seek", "(ii)", 10, 0);
if (res == NULL) {
        ... 發生一個例外 ...
}
else {
        Py_DECREF(res);
}

請注意,由於 PyObject_CallObject() 總是需要一個元組作為引數列表,若要呼叫一個不帶引數的函式,要傳遞 "()" 作為格式,並呼叫一個帶有一個引數的函式,將引數括起來在括號中,例如 "(i)"。

我如何捕捉 PyErr_Print() 的輸出(或任何印出到 stdout/stderr 的東西)?

在 Python 程式碼中定義一個支援 write() 方法的物件。將此物件分配給 sys.stdoutsys.stderr。呼叫 print_error,或者只允許標準的回溯機制起作用。然後,輸出將會傳送到你的 write() 方法所指定的位置。

最簡單的方法是使用 io.StringIO 類別:

>>> import io, sys
>>> sys.stdout = io.StringIO()
>>> print('foo')
>>> print('hello world!')
>>> sys.stderr.write(sys.stdout.getvalue())
foo
hello world!

執行相同操作的自定義物件如下所示:

>>> import io, sys
>>> class StdoutCatcher(io.TextIOBase):
...     def __init__(self):
...         self.data = []
...     def write(self, stuff):
...         self.data.append(stuff)
...
>>> import sys
>>> sys.stdout = StdoutCatcher()
>>> print('foo')
>>> print('hello world!')
>>> sys.stderr.write(''.join(sys.stdout.data))
foo
hello world!

如何從 C 存取用 Python 編寫的模組?

你可以取得指向模組物件的指標,如下所示:

module = PyImport_ImportModule("<modulename>");

如果模組還沒有被引入(即它還沒有出現在 sys.modules 中),這會初始化模組;否則它只回傳 sys.modules["<modulename>"] 的值。請注意,它不會將模組輸入任何命名空間——它只會確保它已被初始化並儲存在 sys.modules 中。

然後你可以存取模組的屬性(即模組中定義的任何名稱),如下所示:

attr = PyObject_GetAttrString(module, "<attrname>");

呼叫 PyObject_SetAttrString() 來分配模組中的變數也有效。

我如何從 Python 介接到 C++ 物件?

根據你的要求不同而有多種不同方法。要手動執行此操作,請先閱讀「擴充和嵌入」說明文件。對於 Python run-time 系統,C 和 C++ 之間並沒有太多區別 —— 因此圍繞 C 結構(指標)型別來構建新 Python 型別的策略也適用於 C++ 物件。

對於 C++ 函式庫,請參閱 寫 C 很難;還有其他選擇嗎?

我使用安裝檔案新增了一個模組,但 make 失敗了;為什麼?

安裝程式必須以換行符結尾,如果那裡沒有換行符,構建過程將失敗。(解決這個問題需要一些醜陋的 shell 腳本 hackery,而且這個錯誤很小,似乎不值得付出努力。)

如何為擴充套件除錯?

將 GDB 與動態載入的擴充一起使用時,在載入擴充之前不能在擴充中設定斷點。

在你的 .gdbinit 檔案中(或交互地),新增命令:

br _PyImport_LoadDynamicModule

然後,當你運行 GDB 時:

$ gdb /local/bin/python
gdb) run myscript.py
gdb) continue # repeat until your extension is loaded
gdb) finish   # so that your extension is loaded
gdb) br myfunction.c:50
gdb) continue

我想在我的 Linux 系統上編譯一個 Python 模組,但是缺少一些檔案。為什麼?

大多數打包版本的 Python 省略了編譯 Python 擴充所需的一些檔案。

在 Red Hat 上,請安裝 python3-devel RPM 來取得必要的檔案。

對於 Debian,運行 apt-get install python3-dev

如何從「無效輸入」區分出「不完整輸入」?

有時你會想模擬 Python 交互式直譯器的行為,當輸入不完整時(例如,當你輸入了 "if" 陳述式的開頭或者你沒有關閉你的括號或三重字串引號)它會給你一個繼續提示字元,但是當輸入無效時,它會立即為你提供語法錯誤訊息。

在 Python 中,你可以使用 codeop 模組,它充分模擬了剖析器 (parser) 的行為。像是 IDLE 就有使用它。

在 C 中執行此操作的最簡單方法是呼叫 PyRun_InteractiveLoop()(可能是在單獨的執行緒中)並讓 Python 直譯器為你處理輸入。你還可以將 PyOS_ReadlineFunctionPointer() 設定為指向你的自定義輸入函式。有關更多提示,請參閱 Modules/readline.cParser/myreadline.c

如何找到未定義的 g++ 符號 __builtin_new 或 __pure_virtual?

要動態載入 g++ 擴充模組,你必須重新編譯 Python,並使用 g++ 重新鏈接它(更改 Python 模組 Makefile 中的 LINKCC),且使用 g++ 鏈接你的擴充模組(例如,g++ -shared -o mymodule.so mymodule.o)。

我可以用一些用 C 實作的方法和用 Python 實作的其他方法(例如透過繼承)建立一個物件類別嗎?

是的,你可以繼承內建類別,例如 intlistdict 等。

Boost Python 函式庫(BPL,https://www.boost.org/libs/python/doc/index.html)提供了一種從 C++ 執行此操作的方法(即你可以使用 BPL 來繼承用 C++ 編寫的擴充類別)。