c++ 内存管理_Python Bindings - 从 Python 调用 C/C++
1. 動態特性導致解釋器低效(python是非常動態的語言,為了支持這些動態特性,付出的是解釋器的低效)
2. python VM 在 GC 方面的低效
3. 由于 GIL 的存在,無法通過多線程支持多核并行計算
4. 沒有 JIT 和更好的 VM,這是相對其它語言來講,比如:Java
所以也就可以圍繞這幾個方面來找到解決方案提升 python 程序的執行速度,還是有些人愿意貢獻自己的時間從這些方面去提升 python 的性能,比如 GIL 的問題,我看到 pycon 2019 這位小伙就分享了他嘗試去解決這個問題。
這篇文章是翻譯自 realpython 上題為 Python Bindings: Calling C or C++ From Python 的文章,怎么繞開 GIL 的限制,怎么避免解釋器的低效,python bindings 是一個方案,也是最常用的方案。
您是要從 Python 使用 C 或 C++ 庫的 Python 開發人員嗎? 如果是這樣,則 Python bindings 允許您調用函數并將數據從 Python 傳遞到 C 或 C++,從而使您能夠充分利用這兩種語言的優勢。 在本教程中,您會看到一些可用于創建 Python bindings 的工具的概述。
在本教程中,您將了解:
- 為什么要從 Python 調用 C 或 C++
- 如何在 C 和 Python 之間傳遞數據
- 哪些工具和方法可以幫助您創建 Python 綁定
本教程針對中級 Python 開發人員。 它假定您具有 Python 的基礎知識,并且對 C 或 C++ 中的函數和數據類型有所了解。 點擊鏈接,您可以獲得本教程中將看到的所有示例代碼。
讓我們深入研究 Python bindings!
Python Bindings 概述
在深入研究如何從 Python 調用 C 之前,最好花點時間了解為什么。 在幾種情況下,創建 Python 綁定來調用 C 庫是一個好主意:
以上所有都是學習創建 Python 綁定 C 庫接口的重要原因。
注意:在本教程中,您將創建與 C 和 C++ 的 Python 綁定。 大多數通用概念都適用于兩種語言,因此除非兩種語言之間有特定區別,否則將使用 C。 通常,每種工具都支持 C 或 C++,但不能同時支持兩者。讓我們開始吧!
編組數據類型(Marshalling Data Types)
等等! 在開始編寫 Python 綁定之前,請查看 Python 和 C 如何存儲數據以及這將導致什么類型的問題。 首先,讓我們定義編組(marshalling)。 Wikipedia 對此概念的定義如下:
將對象的內存表示形式轉換為適合存儲或傳輸的數據格式的過程。原始鏈接出于您的目的,編組是 Python 綁定在準備將數據從 Python 移至 C 或反之時所做的工作。 Python 綁定需要進行編組,因為 Python 和 C 以不同的方式存儲數據。 C 以盡可能緊湊的形式將數據存儲在內存中。 如果使用 uint8_t,則它總共僅使用 8 bits 內存。
另一方面,在 Python 中,一切都是對象。 這意味著每個整數都會在內存中使用很多個字節。 有多少個取決于您正在運行的 Python 版本,您的操作系統以及其他因素。 這意味著您的 Python 綁定需要為跨邊界傳遞的每個整數從 C 整數轉換為 Python 整數。
其他數據類型在兩種語言之間具有相似的關系。 讓我們依次來看一下:
- 整數存儲計數數字。 Python 以任意精度存儲整數,這意味著您可以存儲非常非常大的數字。 C 指定整數的具體大小。 在語言之間切換時,您需要注意數據大小,以防止 Python 整數值溢出 C 整數變量。
- 浮點數是帶小數位的數字。 Python 可以存儲比 C 大得多(小得多)的浮點數。這意味著您還必須注意這些值以確保它們在范圍內。
- 復數是具有虛部的數字。 盡管 Python 具有內置的復數,而 C 具有復雜的數,但沒有內置的方法可在它們之間進行編組。 要編列復數,您需要在 C 代碼中構建一個結構或類來對其進行管理。
- 字符串是字符序列。 對于這種常見的數據類型,在創建 Python 綁定時,字符串會變得非常棘手。 與其他數據類型一樣,Python 和 C 以完全不同的格式存儲字符串。 (與其他數據類型不同,C 和 C++ 在這方面也有所不同,這很有趣!)您將研究的每個解決方案在處理字符串方面都有略有不同的方法。
- 布爾變量只能有兩個值。由于它們在 C 語言中得到支持,因此將它們編組將非常簡單。
除了數據類型轉換外,在構建 Python 綁定時還需要考慮其他問題。讓我們繼續探索它們。
了解可變值和不可變值
除了所有這些數據類型之外,您還必須了解 Python 對象可以是可變和不可變的。 在談論值傳遞或引用傳遞時,C 具有與函數參數類似的概念。 在 C 語言中,所有參數都是傳遞值。 如果要允許函數在調用方中更改變量,則需要將指針傳遞給該變量。
您可能想知道是否可以通過使用指針簡單地將不可變對象傳遞給 C 來解決不可變限制。 除非您進入丑陋且不可攜帶的極端,否則 Python 不會為您提供指向對象的指針,因此這是行不通的。 如果您要在 C 語言中修改 Python 對象,則需要采取額外的步驟來實現。 這些步驟將取決于您使用的工具,你后面會看到。
因此,您可以在項目清單中添加不變性,以便在創建 Python 綁定時考慮。創建此清單的最后一步是如何處理 Python 和 C 處理內存管理的不同方式。
管理內存
C 和 Python 對內存的管理方式不同。 在 C 語言中,開發人員必須管理所有內存分配,并確保一次只能釋放一次。 Python 使用垃圾收集器為您解決此問題。
盡管每種方法都有其優勢,但在創建 Python 綁定時確實增加了額外的麻煩。 您需要了解每個對象的內存分配位置,并確保僅在語言屏障的同一側釋放內存。
例如,當您設置x = 3時,將創建一個 Python 對象。該對象的內存在 Python 端分配,需要進行垃圾回收。 幸運的是,使用 Python 對象,很難做其他任何事情。 看一下 C 語言中的相反情況,您可以在其中直接分配一個內存塊:
int* iPtr = (int*)malloc(sizeof(int));執行此操作時,需要確保在 C 中釋放此指針。這可能意味著需要手動將代碼添加到 Python 綁定中。
這樣就完善了您的常規主題清單。 讓我們開始設置你的系統,以便您編寫一些代碼!
設置環境
在本教程中,您將使用 Real Python GitHub repo 中已經存在的 C 和 C++ 庫來演示每個工具的測試。 目的是您可以將這些構想用于任何 C 庫。 要遵循此處的所有示例,您需要具備以下條件:
- 已安裝 C++ 庫并了解命令行調用的路徑
- Python 開發工具:
- 對于Linux,這是 python3-dev 或 python3-devel 軟件包,具體取決于您的發行版。
- 對于Windows,有多個選項。
- Python 3.6或更高版本
- 一個虛擬環境(推薦,但不是必需的)
- invoke 工具
最后一個可能對您來說是新手,所以讓我們仔細看看。
使用 invoke 工具
invoke是本教程中用于構建和測試 Python 綁定的工具。 它具有類似的用途,但使用 Python 而不是 Makefiles。 您需要使用 pip 在虛擬環境中安裝 invoke:
$ python3 -m pip install invoke要運行它,請鍵入 invoke,然后鍵入要執行的任務:
$ invoke build-cmult ================================================== = Building C Library * Complete要查看可用的任務,請使用 --list 選項:
$ invoke --list Available tasks:all Build and run all testsbuild-cffi Build the CFFI Python bindingsbuild-cmult Build the shared library for the sample C codebuild-cppmult Build the shared library for the sample C++ codebuild-cython Build the cython extension modulebuild-pybind11 Build the pybind11 wrapper libraryclean Remove any built objectstest-cffi Run the script to test CFFItest-ctypes Run the script to test ctypestest-cython Run the script to test Cythontest-pybind11 Run the script to test PyBind11請注意,當您查看定義了調用任務的 task.py 文件時,您會看到列出的第二個任務的名稱為 build_cffi。 但是,--list的輸出將其顯示為 build-cffi。 減號(-)不能用作 Python 名稱的一部分,因此該文件改用下劃線(_)。
對于您要檢查的每種工具,都會定義一個構建和測試任務。 例如,要運行 CFFI 的代碼,可以鍵入 invoke build-cffi test-cffi。 ctypes 是一個例外,因為 ctypes 沒有構建階段。 此外,為方便起見還添加了兩個特殊任務:
- invoke all 運行所有工具的構建和測試任務。
- invoke clean 刪除所有生成的文件。
現在,您已經了解了如何運行代碼,讓我們先看一下要包裝的 C 代碼,然后再訪問工具概述。
C 或 C++ 源代碼
在下面的每個示例部分中,您將為 C 或 C++ 中的同一函數創建 Python 綁定。 這些部分旨在讓您了解每種方法的外觀,而不是該工具的深入教程,因此您要包裝的功能很小。 您將為其創建 Python 綁定的函數將一個整數和一個浮點數作為輸入參數,并返回一個浮點數,該浮點數是兩個數字的乘積:
// cmult.c float cmult(int int_param, float float_param) {float return_value = int_param * float_param;printf(" In cmult : int: %d float %.1f returning %.1fn", int_param,float_param, return_value);return return_value; }C 和 C++ 函數幾乎完全相同,它們之間的名稱和字符串有所不同。 您可以通過單擊鏈接獲得所有代碼的副本:
現在,您已經克隆了庫并安裝了工具,您可以構建和測試工具。因此,讓我們深入了解下面的每個部分!
ctypes
您將從 ctypes 開始,這是標準庫中用于創建 Python 綁定的工具。 它提供了一個低級工具集,用于在 Python 和 C 之間加載共享庫并編組數據。
如何安裝
ctypes 的一大優點是它是 Python 標準庫的一部分。 它是在 Python 2.5 版中添加的,因此你很可能已經有了。 您可以像使用 sys 或 time 模塊一樣導入它。
調用方法
加載 C 庫并調用該函數的所有代碼都在您的 Python 程序中。 很好,因為您的過程沒有多余的步驟。 您只需運行您的程序,一切都將得到照顧。 要在 ctypes 中創建 Python 綁定,您需要執行以下步驟:
您將依次查看每一個。
加載庫
ctypes 提供了幾種加載共享庫的方法,其中一些是特定于平臺的。 以您的示例為例,您將直接通過完整路徑傳遞所需的共享庫來創建 ctypes.CDLL 對象:
# ctypes_test.py import ctypes import pathlibif __name__ == "__main__":# Load the shared library into ctypeslibname = pathlib.Path().absolute() / "libcmult.so"c_lib = ctypes.CDLL(libname)這適用于共享庫與 Python 腳本位于同一目錄中的情況,但是當您嘗試從 Python 綁定以外的包中加載庫時要小心。 在 ctypes 文檔中有許多有關平臺和特定情況的加載庫和查找路徑的詳細信息。
注意:庫加載期間可能會出現許多特定于平臺的問題。實例生效后,最好進行增量更改。現在您已將庫加載到 Python 中,可以嘗試調用它了!
調用你編寫的方法
請記住,您的C函數的函數原型如下:
// cmult.h float cmult(int int_param, float float_param);您需要傳入一個整數和一個浮點數,并且可以期望返回一個浮點數。 整數和浮點數在 Python 和 C 中都具有原生支持,因此您希望這種情況適用于合理的值。
將庫加載到 Python 綁定中后,該函數將成為 c_lib 的屬性,c_lib 是您之前創建的 CDLL 對象。 您可以嘗試這樣稱呼它:
x, y = 6, 2.3 answer = c_lib.cmult(x, y)糟糕!這行不通。在示例 repo 中,此行已被注釋掉,因為它失敗了。如果您嘗試通過該調用運行,則 Python 會報錯:
$ invoke test-ctypes Traceback (most recent call last):File "ctypes_test.py", line 16, in <module>answer = c_lib.cmult(x, y) ctypes.ArgumentError: argument 2: <class 'TypeError'>: Don't know how to convert parameter 2看來您需要告訴 ctypes 任何不是整數的參數。 除非您明確告訴 ctypes,否則 ctypes 對該函數一無所知。 除非另有說明,否則任何參數均假定為整數。 ctypes 不知道如何將 y 中存儲的值 2.3 轉換為整數,因此失敗。
要解決此問題,您需要根據數字創建一個 c_float。您可以在調用該函數的行中執行以下操作:
# ctypes_test.py answer = c_lib.cmult(x, ctypes.c_float(y)) print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")現在,當您運行此代碼時,它將返回您傳入的兩個數字的乘積:
$ invoke test-ctypesIn cmult : int: 6 float 2.3 returning 13.8In Python: int: 6 float 2.3 return val 48.0請稍等... 6 乘以 2.3 不是 48.0!
事實證明,與輸入參數一樣,ctypes 假定您的函數返回一個 int 值。 實際上,您的函數返回一個浮點數,該浮點數被錯誤地編組。 就像輸入參數一樣,您需要告訴 ctypes 使用其他類型。 這里的語法略有不同:
# ctypes_test.py c_lib.cmult.restype = ctypes.c_float answer = c_lib.cmult(x, ctypes.c_float(y)) print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")這應該夠了吧。 讓我們運行整個 test-ctypes 目標,看看有什么。 請記住,輸出的第一部分是在將函數的 restype 固定為 float 之前:
$ invoke test-ctypes ================================================== = Building C Library * Complete ================================================== = Testing ctypes ModuleIn cmult : int: 6 float 2.3 returning 13.8In Python: int: 6 float 2.3 return val 48.0In cmult : int: 6 float 2.3 returning 13.8In Python: int: 6 float 2.3 return val 13.8這樣更好! 當第一個未經更正的版本返回錯誤值時,您的固定版本同意 C 函數。 C 和 Python 都得到相同的結果! 現在它可以正常工作了,看看為什么您可能不希望使用 ctypes。
長處和短處
與其他工具相比,ctypes 的最大優點是它內置在標準庫中。它也不需要任何額外的步驟,因為所有工作都是在 Python 程序中完成的。
此外,所使用的概念是低級的,這使您剛進行的練習變得易于管理。 但是,由于缺乏自動化,更復雜的任務變得繁瑣。 在下一節中,您將看到一個為流程增加一些自動化的工具。
CFFI
CFFI 是 Python 的 C 外部函數接口。 它采用一種更加自動化的方法來生成 Python 綁定。 CFFI 有多種構建和使用 Python 綁定的方式。 有兩個不同的選項可供選擇,這為您提供了四種可能的模式:
- ABI vs API: API 模式使用 C 編譯器生成完整的 Python 模塊,而 ABI 模式加載共享庫并直接與其交互。 如果不運行編譯器,則正確構造結構和參數很容易出錯。 文檔強烈建議使用 API 模式。
- in-line vs out-of-line: 這兩種模式之間的區別在于速度和便利性之間的權衡:
- In-line mode 在每次腳本運行時,都會編譯 Python 綁定。這很方便,因為您不需要額外的構建步驟。但是,它的確會使您的程序變慢。
- Out-of-line mode 需要一個額外的步驟來一次生成 Python 綁定,然后在每次運行程序時都使用它們。這要快得多,但是對于您的應用程序可能并不重要。
在此示例中,您將使用 API?? out-of-line mode,該模式會生成最快的代碼,并且通常看起來與您將在本教程后面創建的其他 Python 綁定類似。
如何安裝
由于 CFFI 不是標準庫的一部分,因此您需要將其安裝在計算機上。 建議您為此創建一個虛擬環境。 幸運的是,CFFI 使用 pip 進行安裝:
$ python3 -m pip install cffi這會將軟件包安裝到您的虛擬環境中。如果您已經從 requirements.txt 安裝了該文件,則應注意這一點。您可以通過訪問鏈接中的 repo 來查看requirements.txt:
現在,您已經安裝了 CFFI,現在該試一下了!
調用方法
與 ctypes 不同,使用 CFFI,您可以創建完整的 Python 模塊。 您可以像導入標準庫中的任何其他模塊一樣導入該模塊。 您需要做一些額外的工作來構建 Python 模塊。 要使用 CFFI Python 綁定,您需要執行以下步驟:
- 編寫一些描述綁定的 Python 代碼。
- 運行該代碼以生成可加載模塊。
- 修改調用代碼以導入和使用新創建的模塊。
這似乎是一項艱巨的工作,但您將逐步完成這些步驟,并了解其工作原理。
編寫綁定
CFFI 提供了一些方法,可以在生成 Python 綁定時讀取 C 頭文件來完成大部分工作。 在 CFFI 文檔中,執行此操作的代碼放置在單獨的 Python 文件中。 在此示例中,您會將代碼直接放入使用 Python 文件作為輸入的構建工具調用中。 要使用 CFFI,首先創建一個 cffi.FFI 對象,該對象提供了所需的三種方法:
# tasks.py import cffi ... """ Build the CFFI Python bindings """ print_banner("Building CFFI Module") ffi = cffi.FFI()獲得FFI后,您將使用 .cdef() 自動處理頭文件的內容。這將為您創建包裝器方法,用來編組來自 Python 的數據:
# tasks.py this_dir = pathlib.Path().absolute() h_file_name = this_dir / "cmult.h" with open(h_file_name) as h_file:ffi.cdef(h_file.read())讀取和處理頭文件是第一步。之后,您需要使用 .set_source() 來描述 CFFI 將生成的源文件:
# tasks.py ffi.set_source("cffi_example",# Since you're calling a fully-built library directly, no custom source# is necessary. You need to include the .h files, though, because behind# the scenes cffi generates a .c file that contains a Python-friendly# wrapper around each of the functions.'#include "cmult.h"',# The important thing is to include the pre-built lib in the list of# libraries you're linking against:libraries=["cmult"],library_dirs=[this_dir.as_posix()],extra_link_args=["-Wl,-rpath,."], )以下是您要傳遞的參數的明細:
- cffi_example 是將在文件系統上創建的源文件的基本名稱。 CFFI 將生成一個 .c 文件,將其編譯為 .o 文件,并將其鏈接至 .so 或 .dll 文件。
- #include "cmult.h" 是自定義 C 源代碼,在編譯之前將包含在生成的源代碼中。在這里,您僅包含要為其生成綁定的 .h 文件,但這可用于一些有趣的自定義。
- library = ["cmult"] 告訴鏈接器您先前存在的 C 庫的名稱。這是一個列表,因此您可以根據需要指定幾個庫。
- library_dirs = [this_dir.as_posix(),] 是目錄列表,它告訴鏈接程序在何處查找上述庫列表。
- extra_link_args = ['-Wl,-rpath,.'] 是一組選項,它們生成一個共享庫,該共享庫將在當前路徑(.)中查找需要加載的其他庫。
構建 Python 綁定
調用 .set_source() 不會建立 Python 綁定。它僅設置元數據來描述將要生成的內容。要構建 Python 綁定,您需要調用 .compile():
# tasks.py ffi.compile()這通過生成 .c 文件,.o 文件和共享庫來完成。您剛瀏覽過的 invoke 任務可以在命令行上運行以構建 Python 綁定:
$ invoke build-cffi ================================================== = Building C Library * Complete ================================================== = Building CFFI Module * Complete您已經有了 CFFI Python 綁定,因此現在該運行該代碼了!
調用你編寫的方法
在完成所有工作之后,您配置并運行了 CFFI 編譯器,使用生成的 Python 綁定看起來就像使用任何其他 Python 模塊一樣:
# cffi_test.py import cffi_exampleif __name__ == "__main__":# Sample data for your callx, y = 6, 2.3answer = cffi_example.lib.cmult(x, y)print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")導入新模塊,然后可以直接調用 cmult()。要測試它,請使用 test-cffi 任務:
$ invoke test-cffi ================================================== = Testing CFFI ModuleIn cmult : int: 6 float 2.3 returning 13.8In Python: int: 6 float 2.3 return val 13.8這將運行 cffi_test.py 程序,該程序將測試您使用 CFFI 創建的新 Python 綁定。這樣就完成了有關編寫和使用 CFFI Python 綁定的部分。
長處和短處
似乎 ctypes 比您剛剛看到的 CFFI 示例所需的工作更少。盡管在這種用例中確實如此,但由于許多功能包裝的自動化,CFFI 可以比 ctypes 更好地擴展到較大的項目。
CFFI 還產生了完全不同的用戶體驗。 ctypes 允許您將預先存在的 C 庫直接加載到 Python 程序中。另一方面,CFFI 創建了一個新的 Python 模塊,該模塊可以像其他 Python 模塊一樣加載。
更重要的是,通過上面使用的 out-of-line API 方法,創建 Python 綁定的時間代價是在構建它時就完成一次,而在每次運行代碼時都不會發生。 對于小型程序,這可能沒什么大不了的,但是 CFFI 也可以通過這種方式更好地擴展到較大的項目。
與 ctypes 一樣,使用 CFFI 僅允許您直接與 C 庫連接。 C++ 庫需要大量工作才能使用。在下一節中,您將看到專注于 C++ 的 Python 綁定工具。
PyBind11
PyBind11 采用了完全不同的方法來創建 Python 綁定。 除了將重點從 C 轉移到 C++ 之外,它還使用 C++ 來指定和構建模塊,從而使其能夠利用 C++ 中的元編程工具。 與 CFFI 一樣,從 PyBind11 生成的 Python 綁定是一個完整的 Python 模塊,可以直接導入和使用。
PyBind11 是在 Boost::Python 庫之后建模的,并具有類似的接口。 它將它的使用限制在 C++11 和更高版本中,但是,與支持所有功能的 Boost 相比,它可以簡化并加快處理速度。
如何安裝
PyBind11 文檔的第一步部分將引導您逐步了解如何下載和構建 PyBind11 的測試用例。 盡管似乎并非嚴格要求這樣做,但按照以下步驟進行操作可以確保您設置正確的 C++ 和 Python 工具。
注意:PyBind11的大多數示例都使用 cmake,這是構建 C 和 C++ 項目的好工具。 但是,對于此演示,您將繼續使用調用工具,該工具將遵循文檔的手動構建部分中的說明。您需要將此工具安裝到虛擬環境中:
$ python3 -m pip install pybind11PyBind11 是一個全標題庫,與 Boost 的大部分相似。 這允許 pip 將庫的實際 C++ 源直接安裝到您的虛擬環境中。
調用方法
在深入研究之前,請注意,您使用的是其他 C++ 源文件 cppmult.cpp,而不是先前示例中使用的 C 文件。 兩種語言的功能基本相同。
編寫綁定
與 CFFI 相似,您需要創建一些代碼來告訴該工具如何構建 Python 綁定。 與 CFFI 不同,此代碼將使用 C++ 而不是 Python。 幸運的是,所需代碼最少:
// pybind11_wrapper.cpp #include <pybind11/pybind11.h> #include <cppmult.hpp>PYBIND11_MODULE(pybind11_example, m) {m.doc() = "pybind11 example plugin"; // Optional module docstringm.def("cpp_function", &cppmult, "A function that multiplies two numbers"); }由于 PyBind11 將大量信息打包到幾行中,因此讓我們一次來看一下。
前兩行包括 pybind11.h 文件和 C++ 庫 cppmult.hpp 的頭文件。之后,您將擁有 PYBIND11_MODULE 宏。這擴展為 PyBind11 源代碼中很好描述的 C++ 代碼塊:
這個宏創建入口點,當 Python 解釋器導入擴展模塊時,該入口點將被調用。 模塊名稱作為第一個參數給出,不應使用引號引起來。 第二個宏參數定義了 py::module 類型的變量,可用于初始化模塊。原始鏈接對您而言,這意味著在本示例中,您正在創建一個名為 pybind11_example 的模塊,其余代碼將使用 m 作為 py::module 對象的名稱。 在下一行中,在您定義的 C++ 函數中,為模塊創建一個文檔字符串。 雖然這是可選的,但可以使您的模塊更加 Pythonic。
最后,您有 m.def() 調用。 這將定義由您的新 Python 綁定導出的函數,這意味著它將在 Python 中可見。 在此示例中,您傳遞了三個參數:
- cpp_function 是您將在 Python 中使用的函數的導出名稱。 如本例所示,它不需要匹配 C++ 函數的名稱。
- &cppmult 使用要導出的函數的地址。
- "A function..." 是該函數的可選文檔字符串。
既然您已經有了 Python 綁定的代碼,請看一下如何將其構建到 Python 模塊中。
構建 python 綁定
在 PyBind11 中用于構建 Python 綁定的工具是 C++ 編譯器本身。您可能需要修改編譯器和操作系統的默認設置。
首先,您必須構建要為其創建綁定的 C++ 庫。 例如,您可以將 cppmult 庫直接構建到 Python 綁定庫中。 但是,對于大多數實際示例,您將擁有一個要包裝的預先存在的庫,因此您將單獨構建 cppmult 庫。 該構建是對編譯器的一個標準調用,以構建一個共享庫:
# tasks.py invoke.run("g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC cppmult.cpp ""-o libcppmult.so " )使用 invoke build-cppmult 運行它會生成 libcppmult.so:
$ invoke build-cppmult ================================================== = Building C++ Library * Complete另一方面,Python 綁定的構建需要一些特殊的細節:
# tasks.py invoke.run("g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC ""`python3 -m pybind11 --includes` ""-I /usr/include/python3.7 -I . ""{0} ""-o {1}`python3.7-config --extension-suffix` ""-L. -lcppmult -Wl,-rpath,.".format(cpp_name, extension_name) )讓我們逐行瀏覽。 第 3 行包含相當標準的 C++ 編譯器標志,這些標志指示一些詳細信息,包括您希望捕獲所有警告并將其視為錯誤,需要共享庫以及正在使用 C++11。
第 4 行是魔術的第一步。它調用 pybind11 模塊,使其為 PyBind11 生成正確的包含路徑。您可以直接在控制臺上運行此命令以查看其作用:
$ python3 -m pybind11 --includes -I/home/jima/.virtualenvs/realpython/include/python3.7m -I/home/jima/.virtualenvs/realpython/include/site/python3.7您的輸出應該相似,但顯示不同的路徑。
在編譯調用的第 5 行中,您可以看到您還在將路徑添加到 Python 開發人員包含的文件中。 建議您不要鏈接到 Python 庫本身,但是源代碼需要 Python.h 中的一些代碼才能發揮其魔力。 幸運的是,它使用的代碼在 Python 版本之間相當穩定。
第5行還使用 -I. 將當前目錄添加到包含路徑列表中。這樣可以解析包裝代碼中的 #include <cppmult.hpp>行。
第 6 行指定源文件的名稱,即 pybind11_wrapper.cpp。 然后,在第 7 行上,您將看到更多的構建魔術正在發生。 此行指定輸出文件的名稱。 Python 在模塊命名方面有一些特別的想法,包括 Python 版本,機器架構和其他細節。 Python 還提供了一個工具來幫助解決這個問題,稱為 python3.7-config:
$ python3.7-config --extension-suffix .cpython-37m-x86_64-linux-gnu.so如果您使用的是其他版本的 Python,則可能需要修改命令。 如果您使用其他版本的 Python 或使用其他操作系統,則結果可能會發生變化。
構建命令的最后一行(第 8 行)將鏈接器指向您之前構建的 libcppmult 庫。 rpath 部分告訴鏈接器將信息添加到共享庫,以幫助操作系統在運行時找到 libcppmult。 最后,您會注意到該字符串的格式為 cpp_name 和 extension_name。 在下一部分中使用 Cython 構建 Python 綁定模塊時,將再次使用此功能。
$ invoke build-pybind11 ================================================== = Building C++ Library * Complete ================================================== = Building PyBind11 Module * Complete是的!您已經使用 PyBind11 構建了 Python 綁定。現在該進行測試了!
調用你編寫的方法
與上面的 CFFI 示例類似,完成創建 Python 綁定的繁重工作后,調用函數看起來就像普通的 Python 代碼:
# pybind11_test.py import pybind11_exampleif __name__ == "__main__":# Sample data for your callx, y = 6, 2.3answer = pybind11_example.cpp_function(x, y)print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")由于您在 PYBIND11_MODULE 宏中使用了 pybind11_example 作為模塊的名稱,因此,這就是您導入的名稱。 在 m.def() 調用中,您告訴 PyBind11 將 cppmult 函數導出為 cpp_function,這就是從 Python 調用它的方法。
您也可以使用 invoke 對其進行測試:
$ invoke test-pybind11 ================================================== = Testing PyBind11 ModuleIn cppmul: int: 6 float 2.3 returning 13.8In Python: int: 6 float 2.3 return val 13.8這就是 PyBind11 的樣子。接下來,您將了解何時以及為什么 PyBind11 是適合該工作的工具。
長處和短處
PyBind11 專注于 C++ 而不是 C,這使其不同于 ctypes 和 CFFI。它具有一些功能,使其對于 C++ 庫非常有吸引力:
- 它支持類。
- 它處理多態子類化。
- 它使您可以從 Python 和許多其他工具向對象添加動態屬性,而這對于您已經研究過的基于 C 的工具來說是很難做到的。
話雖這么說,您需要做一些設置和配置來啟動和運行 PyBind11。 正確安裝和構建可能有些棘手,但是一旦完成,它似乎就很牢固了。 另外,PyBind11 要求您至少使用 C++11 或更高版本。 對于大多數項目而言,這不太可能成為一個很大的限制,但您可能需要考慮。
最后,創建 Python 綁定所需編寫的額外代碼是 C++,而不是 Python。 這可能對您來說不是問題,但與您在此處看到的跟其他工具不同。 在下一節中,您將繼續學習 Cython,它采用了完全不同的方法來解決此問題。
Cython
Cython 用于創建 Python 綁定的方法,使用類似于 Python 的語言來定義綁定,然后生成可編譯到模塊中的 C 或 C++ 代碼。 有多種使用 Cython 構建 Python 綁定的方法。 最常見的一種是使用 distutils 中的安裝程序。 在此示例中,您將繼續使用 invoke 工具,該工具將使您能夠精確執行所運行的命令。
如何安裝
Cython 是一個 Python 模塊,可以從 PyPI 安裝到您的虛擬環境中:
$ python3 -m pip install cython同樣,如果您已經將 requirements.txt 文件安裝到虛擬環境中,則該文件已經存在。您可以通過單擊鏈接來獲取requirements.txt 的副本。
那應該已經準備好與 Cython 合作!
調用方法
要使用 Cython 構建 Python 綁定,您將遵循與用于 CFFI 和 PyBind11 的步驟相似的步驟。 您將編寫綁定,構建它們,然后運行 Python 代碼來調用它們。 Cython 可以同時支持 C 和 C++。 在此示例中,您將使用在上面的 PyBind11 示例中使用的 cppmult 庫。
編寫綁定
在 Cython 中聲明模塊的最常見形式是使用 .pyx 文件:
# cython_example.pyx """ Example cython interface definition """cdef extern from "cppmult.hpp":float cppmult(int int_param, float float_param)def pymult( int_param, float_param ):return cppmult( int_param, float_param )這里有兩個部分:
- 第 3 行和第 4 行告訴 Cython 您正在使用 cppmult.hpp 中的 cppmult()。
- 第 6 行和第 7 行創建了包裝函數 pymult(),以調用 cppmult()。
這里使用的語言是 C,C++ 和 Python 的特殊組合。 不過,對于 Python 開發人員來說,它看起來相當熟悉,因為其目標是使過程變得更容易。
使用 cdef extern... 的第一部分告訴 Cython,以下函數聲明也在 cppmult.hpp 文件中找到。 這對于確保針對與 C++ 代碼相同的聲明構建 Python 綁定很有用。 第二部分看起來像是常規的 Python 函數-因為它是! 本部分創建可訪問 C++ 函數 cppmult 的 Python 函數。
現在您已經定義了 Python 綁定,是時候構建它們了!
構建 python 綁定
Cython 的構建過程與您用于 PyBind11 的過程相似。您首先在 .pyx 文件上運行 Cython 以生成一個 .cpp 文件。完成此操作后,可以使用與 PyBind11 相同的功能對其進行編譯:
# tasks.py def compile_python_module(cpp_name, extension_name):invoke.run("g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC ""`python3 -m pybind11 --includes` ""-I /usr/include/python3.7 -I . ""{0} ""-o {1}`python3.7-config --extension-suffix` ""-L. -lcppmult -Wl,-rpath,.".format(cpp_name, extension_name))def build_cython(c):""" Build the cython extension module """print_banner("Building Cython Module")# Run cython on the pyx file to create a .cpp fileinvoke.run("cython --cplus -3 cython_example.pyx -o cython_wrapper.cpp")# Compile and link the cython wrapper librarycompile_python_module("cython_wrapper.cpp", "cython_example")print("* Complete")首先在 .pyx 文件上運行 cython。您可以在此命令上使用一些選項:
- --cplus告訴編譯器生成 C++ 文件而不是 C 文件。
- -3 切換 Cython 生成 Python 3 語法,而不是 Python 2。
- -o cython_wrapper.cpp 指定要生成的文件的名稱。
生成 C++ 文件后,就像使用 PyBind11 一樣,您可以使用 C++ 編譯器生成 Python 綁定。 請注意,使用 pybind11 工具生成額外包含路徑的調用仍在該函數中。 在這里不會造成任何傷害,因為您的源碼將不需要這些。
在 invoke 中運行此任務將產生以下輸出:
$ invoke build-cython ================================================== = Building C++ Library * Complete ================================================== = Building Cython Module * Complete您會看到它構建了 cppmult 庫,然后構建了 cython 模塊來包裝它。現在您有了 Cython Python 綁定。(嘗試快速地說出……),現在該進行測試了!
調用你編寫的方法
調用新的 Python 綁定的 Python 代碼與用于測試其他模塊的代碼非常相似:
# cython_test.py import cython_example# Sample data for your call x, y = 6, 2.3answer = cython_example.pymult(x, y) print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")第 2 行將導入新的 Python 綁定模塊,并在第 7 行調用 pymult()。請記住,.pyx 文件在 cppmult() 周圍提供了 Python 包裝器,并將其重命名為 pymult。使用 invoke 運行測試將產生以下結果:
$ invoke test-cython ================================================== = Testing Cython ModuleIn cppmul: int: 6 float 2.3 returning 13.8In Python: int: 6 float 2.3 return val 13.8您得到與以前相同的結果!
長處和短處
Cython 是一個相對復雜的工具,在為 C 或 C++ 創建 Python 綁定時,可以為您提供更深層次的控制。 盡管這里沒有詳細介紹,但它提供了一種 Python 風格的方法來編寫可手動控制 GIL 的代碼,從而可以顯著加快某些類型的問題。
但是,這種 Python 風格的語言并不完全是 Python,因此,當您逐漸確定 C 和 Python 的哪些部分適合您時,會有一條學習曲線。
其它一些解決方案
在研究本教程時,我遇到了幾種用于創建 Python 綁定的工具和選項。 盡管我將本概述限制為一些更常見的選項,但我偶然發現了其他幾種工具。 以下列表并不全面。 如果上述工具之一不適合您的項目,則僅是其他可能性的示例。
PyBindGen
PyBindGen 生成用于 C 或 C++ 的 Python 綁定,并用 Python 編寫。 它的目標是產生可讀的 C 或 C++ 代碼,這將簡化調試問題。 目前尚不清楚是否最近更新,因為該文檔將 Python 3.4 列為最新測試版本。 但是,最近幾年每年都有版本發布。
Boost.Python
Boost.Python 具有類似于您在上面看到的 PyBind11 的接口。 這不是巧合,因為 PyBind11 是基于該庫的! Boost.Python 是用完整的 C++ 編寫的,并且在大多數平臺上支持大多數(如果不是全部)C++ 版本。 相比之下,PyBind11 僅限于使用現代 C++。
SIP
SIP 是為 PyQt 項目開發的用于生成 Python 綁定的工具集。 wxPython 項目也使用它來生成其綁定。 它具有代碼生成工具和一個額外的 Python 模塊,該模塊為生成的代碼提供支持功能。
Cppyy
cppyy 是一個有趣的工具,其設計目標與到目前為止所看到的略有不同。用包作者的話來說:
“cppyy 的原始想法(可追溯到2001年)是允許生活在 C++ 世界中的 Python 程序員訪問那些 C++ 程序包,而不必直接接觸 C++(或等待 C++ 開發人員到來并提供綁定)。” 原始鏈接Shiboken
Shiboken 是一種生成 Python 綁定的工具,該工具是為與 Qt 項目關聯的 PySide 項目開發的。 雖然將其設計為該項目的工具,但文檔顯示它既不是 Qt 也不是 PySide專 用的,并且可以用于其他項目。
SWIG
SWIG 是與此處列出的任何其他工具不同的工具。 這是一種通用工具,可用于創建與許多其他語言(不僅限于 Python)的 C 和 C++ 程序的綁定。 在某些項目中,這種為不同語言生成綁定的功能可能會非常有用。 就復雜性而言,當然要付出代價。
結論
恭喜! 現在,您已經對用于創建 Python 綁定的幾種不同選項有了一定的概覽。 您已經了解了編組數據和創建綁定時需要考慮的問題。 您已經了解了使用以下工具可以從 Python 調用 C 或 C++ 函數的過程:
- ctypes
- CFFI
- PyBind11
- Cython
您現在已經知道,盡管 ctypes 允許您直接加載 DLL 或共享庫,但其他三個工具需要額外的步驟,但仍會創建完整的 Python 模塊。 另外,您還可以使用 invoke 工具從 Python 側運行命令行任務。 單擊鏈接可以獲取在本教程中看到的所有代碼。
現在選擇您喜歡的工具并開始構建這些 Python 綁定吧!特別感謝 Loic Domaigne 對本教程的額外技術評論。
總結
以上是生活随笔為你收集整理的c++ 内存管理_Python Bindings - 从 Python 调用 C/C++的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql cmd 实时监控_MySQL
- 下一篇: java byte 判断相等_你真的了解