c++ 内存管理_Python Bindings - 从 Python 调用 C/C++
1. 動態(tài)特性導(dǎo)致解釋器低效(python是非常動態(tài)的語言,為了支持這些動態(tài)特性,付出的是解釋器的低效)
2. python VM 在 GC 方面的低效
3. 由于 GIL 的存在,無法通過多線程支持多核并行計算
4. 沒有 JIT 和更好的 VM,這是相對其它語言來講,比如:Java
所以也就可以圍繞這幾個方面來找到解決方案提升 python 程序的執(zhí)行速度,還是有些人愿意貢獻自己的時間從這些方面去提升 python 的性能,比如 GIL 的問題,我看到 pycon 2019 這位小伙就分享了他嘗試去解決這個問題。
這篇文章是翻譯自 realpython 上題為 Python Bindings: Calling C or C++ From Python 的文章,怎么繞開 GIL 的限制,怎么避免解釋器的低效,python bindings 是一個方案,也是最常用的方案。
您是要從 Python 使用 C 或 C++ 庫的 Python 開發(fā)人員嗎? 如果是這樣,則 Python bindings 允許您調(diào)用函數(shù)并將數(shù)據(jù)從 Python 傳遞到 C 或 C++,從而使您能夠充分利用這兩種語言的優(yōu)勢。 在本教程中,您會看到一些可用于創(chuàng)建 Python bindings 的工具的概述。
在本教程中,您將了解:
- 為什么要從 Python 調(diào)用 C 或 C++
- 如何在 C 和 Python 之間傳遞數(shù)據(jù)
- 哪些工具和方法可以幫助您創(chuàng)建 Python 綁定
本教程針對中級 Python 開發(fā)人員。 它假定您具有 Python 的基礎(chǔ)知識,并且對 C 或 C++ 中的函數(shù)和數(shù)據(jù)類型有所了解。 點擊鏈接,您可以獲得本教程中將看到的所有示例代碼。
讓我們深入研究 Python bindings!
Python Bindings 概述
在深入研究如何從 Python 調(diào)用 C 之前,最好花點時間了解為什么。 在幾種情況下,創(chuàng)建 Python 綁定來調(diào)用 C 庫是一個好主意:
以上所有都是學(xué)習(xí)創(chuàng)建 Python 綁定 C 庫接口的重要原因。
注意:在本教程中,您將創(chuàng)建與 C 和 C++ 的 Python 綁定。 大多數(shù)通用概念都適用于兩種語言,因此除非兩種語言之間有特定區(qū)別,否則將使用 C。 通常,每種工具都支持 C 或 C++,但不能同時支持兩者。讓我們開始吧!
編組數(shù)據(jù)類型(Marshalling Data Types)
等等! 在開始編寫 Python 綁定之前,請查看 Python 和 C 如何存儲數(shù)據(jù)以及這將導(dǎo)致什么類型的問題。 首先,讓我們定義編組(marshalling)。 Wikipedia 對此概念的定義如下:
將對象的內(nèi)存表示形式轉(zhuǎn)換為適合存儲或傳輸?shù)臄?shù)據(jù)格式的過程。原始鏈接出于您的目的,編組是 Python 綁定在準(zhǔn)備將數(shù)據(jù)從 Python 移至 C 或反之時所做的工作。 Python 綁定需要進行編組,因為 Python 和 C 以不同的方式存儲數(shù)據(jù)。 C 以盡可能緊湊的形式將數(shù)據(jù)存儲在內(nèi)存中。 如果使用 uint8_t,則它總共僅使用 8 bits 內(nèi)存。
另一方面,在 Python 中,一切都是對象。 這意味著每個整數(shù)都會在內(nèi)存中使用很多個字節(jié)。 有多少個取決于您正在運行的 Python 版本,您的操作系統(tǒng)以及其他因素。 這意味著您的 Python 綁定需要為跨邊界傳遞的每個整數(shù)從 C 整數(shù)轉(zhuǎn)換為 Python 整數(shù)。
其他數(shù)據(jù)類型在兩種語言之間具有相似的關(guān)系。 讓我們依次來看一下:
- 整數(shù)存儲計數(shù)數(shù)字。 Python 以任意精度存儲整數(shù),這意味著您可以存儲非常非常大的數(shù)字。 C 指定整數(shù)的具體大小。 在語言之間切換時,您需要注意數(shù)據(jù)大小,以防止 Python 整數(shù)值溢出 C 整數(shù)變量。
- 浮點數(shù)是帶小數(shù)位的數(shù)字。 Python 可以存儲比 C 大得多(小得多)的浮點數(shù)。這意味著您還必須注意這些值以確保它們在范圍內(nèi)。
- 復(fù)數(shù)是具有虛部的數(shù)字。 盡管 Python 具有內(nèi)置的復(fù)數(shù),而 C 具有復(fù)雜的數(shù),但沒有內(nèi)置的方法可在它們之間進行編組。 要編列復(fù)數(shù),您需要在 C 代碼中構(gòu)建一個結(jié)構(gòu)或類來對其進行管理。
- 字符串是字符序列。 對于這種常見的數(shù)據(jù)類型,在創(chuàng)建 Python 綁定時,字符串會變得非常棘手。 與其他數(shù)據(jù)類型一樣,Python 和 C 以完全不同的格式存儲字符串。 (與其他數(shù)據(jù)類型不同,C 和 C++ 在這方面也有所不同,這很有趣!)您將研究的每個解決方案在處理字符串方面都有略有不同的方法。
- 布爾變量只能有兩個值。由于它們在 C 語言中得到支持,因此將它們編組將非常簡單。
除了數(shù)據(jù)類型轉(zhuǎn)換外,在構(gòu)建 Python 綁定時還需要考慮其他問題。讓我們繼續(xù)探索它們。
了解可變值和不可變值
除了所有這些數(shù)據(jù)類型之外,您還必須了解 Python 對象可以是可變和不可變的。 在談?wù)撝祩鬟f或引用傳遞時,C 具有與函數(shù)參數(shù)類似的概念。 在 C 語言中,所有參數(shù)都是傳遞值。 如果要允許函數(shù)在調(diào)用方中更改變量,則需要將指針傳遞給該變量。
您可能想知道是否可以通過使用指針簡單地將不可變對象傳遞給 C 來解決不可變限制。 除非您進入丑陋且不可攜帶的極端,否則 Python 不會為您提供指向?qū)ο蟮闹羔?#xff0c;因此這是行不通的。 如果您要在 C 語言中修改 Python 對象,則需要采取額外的步驟來實現(xiàn)。 這些步驟將取決于您使用的工具,你后面會看到。
因此,您可以在項目清單中添加不變性,以便在創(chuàng)建 Python 綁定時考慮。創(chuàng)建此清單的最后一步是如何處理 Python 和 C 處理內(nèi)存管理的不同方式。
管理內(nèi)存
C 和 Python 對內(nèi)存的管理方式不同。 在 C 語言中,開發(fā)人員必須管理所有內(nèi)存分配,并確保一次只能釋放一次。 Python 使用垃圾收集器為您解決此問題。
盡管每種方法都有其優(yōu)勢,但在創(chuàng)建 Python 綁定時確實增加了額外的麻煩。 您需要了解每個對象的內(nèi)存分配位置,并確保僅在語言屏障的同一側(cè)釋放內(nèi)存。
例如,當(dāng)您設(shè)置x = 3時,將創(chuàng)建一個 Python 對象。該對象的內(nèi)存在 Python 端分配,需要進行垃圾回收。 幸運的是,使用 Python 對象,很難做其他任何事情。 看一下 C 語言中的相反情況,您可以在其中直接分配一個內(nèi)存塊:
int* iPtr = (int*)malloc(sizeof(int));執(zhí)行此操作時,需要確保在 C 中釋放此指針。這可能意味著需要手動將代碼添加到 Python 綁定中。
這樣就完善了您的常規(guī)主題清單。 讓我們開始設(shè)置你的系統(tǒng),以便您編寫一些代碼!
設(shè)置環(huán)境
在本教程中,您將使用 Real Python GitHub repo 中已經(jīng)存在的 C 和 C++ 庫來演示每個工具的測試。 目的是您可以將這些構(gòu)想用于任何 C 庫。 要遵循此處的所有示例,您需要具備以下條件:
- 已安裝 C++ 庫并了解命令行調(diào)用的路徑
- Python 開發(fā)工具:
- 對于Linux,這是 python3-dev 或 python3-devel 軟件包,具體取決于您的發(fā)行版。
- 對于Windows,有多個選項。
- Python 3.6或更高版本
- 一個虛擬環(huán)境(推薦,但不是必需的)
- invoke 工具
最后一個可能對您來說是新手,所以讓我們仔細看看。
使用 invoke 工具
invoke是本教程中用于構(gòu)建和測試 Python 綁定的工具。 它具有類似的用途,但使用 Python 而不是 Makefiles。 您需要使用 pip 在虛擬環(huán)境中安裝 invoke:
$ python3 -m pip install invoke要運行它,請鍵入 invoke,然后鍵入要執(zhí)行的任務(wù):
$ invoke build-cmult ================================================== = Building C Library * Complete要查看可用的任務(wù),請使用 --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請注意,當(dāng)您查看定義了調(diào)用任務(wù)的 task.py 文件時,您會看到列出的第二個任務(wù)的名稱為 build_cffi。 但是,--list的輸出將其顯示為 build-cffi。 減號(-)不能用作 Python 名稱的一部分,因此該文件改用下劃線(_)。
對于您要檢查的每種工具,都會定義一個構(gòu)建和測試任務(wù)。 例如,要運行 CFFI 的代碼,可以鍵入 invoke build-cffi test-cffi。 ctypes 是一個例外,因為 ctypes 沒有構(gòu)建階段。 此外,為方便起見還添加了兩個特殊任務(wù):
- invoke all 運行所有工具的構(gòu)建和測試任務(wù)。
- invoke clean 刪除所有生成的文件。
現(xiàn)在,您已經(jīng)了解了如何運行代碼,讓我們先看一下要包裝的 C 代碼,然后再訪問工具概述。
C 或 C++ 源代碼
在下面的每個示例部分中,您將為 C 或 C++ 中的同一函數(shù)創(chuàng)建 Python 綁定。 這些部分旨在讓您了解每種方法的外觀,而不是該工具的深入教程,因此您要包裝的功能很小。 您將為其創(chuàng)建 Python 綁定的函數(shù)將一個整數(shù)和一個浮點數(shù)作為輸入?yún)?shù),并返回一個浮點數(shù),該浮點數(shù)是兩個數(shù)字的乘積:
// 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++ 函數(shù)幾乎完全相同,它們之間的名稱和字符串有所不同。 您可以通過單擊鏈接獲得所有代碼的副本:
現(xiàn)在,您已經(jīng)克隆了庫并安裝了工具,您可以構(gòu)建和測試工具。因此,讓我們深入了解下面的每個部分!
ctypes
您將從 ctypes 開始,這是標(biāo)準(zhǔn)庫中用于創(chuàng)建 Python 綁定的工具。 它提供了一個低級工具集,用于在 Python 和 C 之間加載共享庫并編組數(shù)據(jù)。
如何安裝
ctypes 的一大優(yōu)點是它是 Python 標(biāo)準(zhǔn)庫的一部分。 它是在 Python 2.5 版中添加的,因此你很可能已經(jīng)有了。 您可以像使用 sys 或 time 模塊一樣導(dǎo)入它。
調(diào)用方法
加載 C 庫并調(diào)用該函數(shù)的所有代碼都在您的 Python 程序中。 很好,因為您的過程沒有多余的步驟。 您只需運行您的程序,一切都將得到照顧。 要在 ctypes 中創(chuàng)建 Python 綁定,您需要執(zhí)行以下步驟:
您將依次查看每一個。
加載庫
ctypes 提供了幾種加載共享庫的方法,其中一些是特定于平臺的。 以您的示例為例,您將直接通過完整路徑傳遞所需的共享庫來創(chuàng)建 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 腳本位于同一目錄中的情況,但是當(dāng)您嘗試從 Python 綁定以外的包中加載庫時要小心。 在 ctypes 文檔中有許多有關(guān)平臺和特定情況的加載庫和查找路徑的詳細信息。
注意:庫加載期間可能會出現(xiàn)許多特定于平臺的問題。實例生效后,最好進行增量更改。現(xiàn)在您已將庫加載到 Python 中,可以嘗試調(diào)用它了!
調(diào)用你編寫的方法
請記住,您的C函數(shù)的函數(shù)原型如下:
// cmult.h float cmult(int int_param, float float_param);您需要傳入一個整數(shù)和一個浮點數(shù),并且可以期望返回一個浮點數(shù)。 整數(shù)和浮點數(shù)在 Python 和 C 中都具有原生支持,因此您希望這種情況適用于合理的值。
將庫加載到 Python 綁定中后,該函數(shù)將成為 c_lib 的屬性,c_lib 是您之前創(chuàng)建的 CDLL 對象。 您可以嘗試這樣稱呼它:
x, y = 6, 2.3 answer = c_lib.cmult(x, y)糟糕!這行不通。在示例 repo 中,此行已被注釋掉,因為它失敗了。如果您嘗試通過該調(diào)用運行,則 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 任何不是整數(shù)的參數(shù)。 除非您明確告訴 ctypes,否則 ctypes 對該函數(shù)一無所知。 除非另有說明,否則任何參數(shù)均假定為整數(shù)。 ctypes 不知道如何將 y 中存儲的值 2.3 轉(zhuǎn)換為整數(shù),因此失敗。
要解決此問題,您需要根據(jù)數(shù)字創(chuàng)建一個 c_float。您可以在調(diào)用該函數(shù)的行中執(zhí)行以下操作:
# 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}")現(xiàn)在,當(dāng)您運行此代碼時,它將返回您傳入的兩個數(shù)字的乘積:
$ 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!
事實證明,與輸入?yún)?shù)一樣,ctypes 假定您的函數(shù)返回一個 int 值。 實際上,您的函數(shù)返回一個浮點數(shù),該浮點數(shù)被錯誤地編組。 就像輸入?yún)?shù)一樣,您需要告訴 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}")這應(yīng)該夠了吧。 讓我們運行整個 test-ctypes 目標(biāo),看看有什么。 請記住,輸出的第一部分是在將函數(shù)的 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這樣更好! 當(dāng)?shù)谝粋€未經(jīng)更正的版本返回錯誤值時,您的固定版本同意 C 函數(shù)。 C 和 Python 都得到相同的結(jié)果! 現(xiàn)在它可以正常工作了,看看為什么您可能不希望使用 ctypes。
長處和短處
與其他工具相比,ctypes 的最大優(yōu)點是它內(nèi)置在標(biāo)準(zhǔn)庫中。它也不需要任何額外的步驟,因為所有工作都是在 Python 程序中完成的。
此外,所使用的概念是低級的,這使您剛進行的練習(xí)變得易于管理。 但是,由于缺乏自動化,更復(fù)雜的任務(wù)變得繁瑣。 在下一節(jié)中,您將看到一個為流程增加一些自動化的工具。
CFFI
CFFI 是 Python 的 C 外部函數(shù)接口。 它采用一種更加自動化的方法來生成 Python 綁定。 CFFI 有多種構(gòu)建和使用 Python 綁定的方式。 有兩個不同的選項可供選擇,這為您提供了四種可能的模式:
- ABI vs API: API 模式使用 C 編譯器生成完整的 Python 模塊,而 ABI 模式加載共享庫并直接與其交互。 如果不運行編譯器,則正確構(gòu)造結(jié)構(gòu)和參數(shù)很容易出錯。 文檔強烈建議使用 API 模式。
- in-line vs out-of-line: 這兩種模式之間的區(qū)別在于速度和便利性之間的權(quán)衡:
- In-line mode 在每次腳本運行時,都會編譯 Python 綁定。這很方便,因為您不需要額外的構(gòu)建步驟。但是,它的確會使您的程序變慢。
- Out-of-line mode 需要一個額外的步驟來一次生成 Python 綁定,然后在每次運行程序時都使用它們。這要快得多,但是對于您的應(yīng)用程序可能并不重要。
在此示例中,您將使用 API?? out-of-line mode,該模式會生成最快的代碼,并且通常看起來與您將在本教程后面創(chuàng)建的其他 Python 綁定類似。
如何安裝
由于 CFFI 不是標(biāo)準(zhǔn)庫的一部分,因此您需要將其安裝在計算機上。 建議您為此創(chuàng)建一個虛擬環(huán)境。 幸運的是,CFFI 使用 pip 進行安裝:
$ python3 -m pip install cffi這會將軟件包安裝到您的虛擬環(huán)境中。如果您已經(jīng)從 requirements.txt 安裝了該文件,則應(yīng)注意這一點。您可以通過訪問鏈接中的 repo 來查看requirements.txt:
現(xiàn)在,您已經(jīng)安裝了 CFFI,現(xiàn)在該試一下了!
調(diào)用方法
與 ctypes 不同,使用 CFFI,您可以創(chuàng)建完整的 Python 模塊。 您可以像導(dǎo)入標(biāo)準(zhǔn)庫中的任何其他模塊一樣導(dǎo)入該模塊。 您需要做一些額外的工作來構(gòu)建 Python 模塊。 要使用 CFFI Python 綁定,您需要執(zhí)行以下步驟:
- 編寫一些描述綁定的 Python 代碼。
- 運行該代碼以生成可加載模塊。
- 修改調(diào)用代碼以導(dǎo)入和使用新創(chuàng)建的模塊。
這似乎是一項艱巨的工作,但您將逐步完成這些步驟,并了解其工作原理。
編寫綁定
CFFI 提供了一些方法,可以在生成 Python 綁定時讀取 C 頭文件來完成大部分工作。 在 CFFI 文檔中,執(zhí)行此操作的代碼放置在單獨的 Python 文件中。 在此示例中,您會將代碼直接放入使用 Python 文件作為輸入的構(gòu)建工具調(diào)用中。 要使用 CFFI,首先創(chuàng)建一個 cffi.FFI 對象,該對象提供了所需的三種方法:
# tasks.py import cffi ... """ Build the CFFI Python bindings """ print_banner("Building CFFI Module") ffi = cffi.FFI()獲得FFI后,您將使用 .cdef() 自動處理頭文件的內(nèi)容。這將為您創(chuàng)建包裝器方法,用來編組來自 Python 的數(shù)據(jù):
# 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,."], )以下是您要傳遞的參數(shù)的明細:
- cffi_example 是將在文件系統(tǒng)上創(chuàng)建的源文件的基本名稱。 CFFI 將生成一個 .c 文件,將其編譯為 .o 文件,并將其鏈接至 .so 或 .dll 文件。
- #include "cmult.h" 是自定義 C 源代碼,在編譯之前將包含在生成的源代碼中。在這里,您僅包含要為其生成綁定的 .h 文件,但這可用于一些有趣的自定義。
- library = ["cmult"] 告訴鏈接器您先前存在的 C 庫的名稱。這是一個列表,因此您可以根據(jù)需要指定幾個庫。
- library_dirs = [this_dir.as_posix(),] 是目錄列表,它告訴鏈接程序在何處查找上述庫列表。
- extra_link_args = ['-Wl,-rpath,.'] 是一組選項,它們生成一個共享庫,該共享庫將在當(dāng)前路徑(.)中查找需要加載的其他庫。
構(gòu)建 Python 綁定
調(diào)用 .set_source() 不會建立 Python 綁定。它僅設(shè)置元數(shù)據(jù)來描述將要生成的內(nèi)容。要構(gòu)建 Python 綁定,您需要調(diào)用 .compile():
# tasks.py ffi.compile()這通過生成 .c 文件,.o 文件和共享庫來完成。您剛瀏覽過的 invoke 任務(wù)可以在命令行上運行以構(gòu)建 Python 綁定:
$ invoke build-cffi ================================================== = Building C Library * Complete ================================================== = Building CFFI Module * Complete您已經(jīng)有了 CFFI Python 綁定,因此現(xiàn)在該運行該代碼了!
調(diào)用你編寫的方法
在完成所有工作之后,您配置并運行了 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}")導(dǎo)入新模塊,然后可以直接調(diào)用 cmult()。要測試它,請使用 test-cffi 任務(wù):
$ 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 程序,該程序?qū)y試您使用 CFFI 創(chuàng)建的新 Python 綁定。這樣就完成了有關(guān)編寫和使用 CFFI Python 綁定的部分。
長處和短處
似乎 ctypes 比您剛剛看到的 CFFI 示例所需的工作更少。盡管在這種用例中確實如此,但由于許多功能包裝的自動化,CFFI 可以比 ctypes 更好地擴展到較大的項目。
CFFI 還產(chǎn)生了完全不同的用戶體驗。 ctypes 允許您將預(yù)先存在的 C 庫直接加載到 Python 程序中。另一方面,CFFI 創(chuàng)建了一個新的 Python 模塊,該模塊可以像其他 Python 模塊一樣加載。
更重要的是,通過上面使用的 out-of-line API 方法,創(chuàng)建 Python 綁定的時間代價是在構(gòu)建它時就完成一次,而在每次運行代碼時都不會發(fā)生。 對于小型程序,這可能沒什么大不了的,但是 CFFI 也可以通過這種方式更好地擴展到較大的項目。
與 ctypes 一樣,使用 CFFI 僅允許您直接與 C 庫連接。 C++ 庫需要大量工作才能使用。在下一節(jié)中,您將看到專注于 C++ 的 Python 綁定工具。
PyBind11
PyBind11 采用了完全不同的方法來創(chuàng)建 Python 綁定。 除了將重點從 C 轉(zhuǎn)移到 C++ 之外,它還使用 C++ 來指定和構(gòu)建模塊,從而使其能夠利用 C++ 中的元編程工具。 與 CFFI 一樣,從 PyBind11 生成的 Python 綁定是一個完整的 Python 模塊,可以直接導(dǎo)入和使用。
PyBind11 是在 Boost::Python 庫之后建模的,并具有類似的接口。 它將它的使用限制在 C++11 和更高版本中,但是,與支持所有功能的 Boost 相比,它可以簡化并加快處理速度。
如何安裝
PyBind11 文檔的第一步部分將引導(dǎo)您逐步了解如何下載和構(gòu)建 PyBind11 的測試用例。 盡管似乎并非嚴(yán)格要求這樣做,但按照以下步驟進行操作可以確保您設(shè)置正確的 C++ 和 Python 工具。
注意:PyBind11的大多數(shù)示例都使用 cmake,這是構(gòu)建 C 和 C++ 項目的好工具。 但是,對于此演示,您將繼續(xù)使用調(diào)用工具,該工具將遵循文檔的手動構(gòu)建部分中的說明。您需要將此工具安裝到虛擬環(huán)境中:
$ python3 -m pip install pybind11PyBind11 是一個全標(biāo)題庫,與 Boost 的大部分相似。 這允許 pip 將庫的實際 C++ 源直接安裝到您的虛擬環(huán)境中。
調(diào)用方法
在深入研究之前,請注意,您使用的是其他 C++ 源文件 cppmult.cpp,而不是先前示例中使用的 C 文件。 兩種語言的功能基本相同。
編寫綁定
與 CFFI 相似,您需要創(chuàng)建一些代碼來告訴該工具如何構(gòu)建 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++ 代碼塊:
這個宏創(chuàng)建入口點,當(dāng) Python 解釋器導(dǎo)入擴展模塊時,該入口點將被調(diào)用。 模塊名稱作為第一個參數(shù)給出,不應(yīng)使用引號引起來。 第二個宏參數(shù)定義了 py::module 類型的變量,可用于初始化模塊。原始鏈接對您而言,這意味著在本示例中,您正在創(chuàng)建一個名為 pybind11_example 的模塊,其余代碼將使用 m 作為 py::module 對象的名稱。 在下一行中,在您定義的 C++ 函數(shù)中,為模塊創(chuàng)建一個文檔字符串。 雖然這是可選的,但可以使您的模塊更加 Pythonic。
最后,您有 m.def() 調(diào)用。 這將定義由您的新 Python 綁定導(dǎo)出的函數(shù),這意味著它將在 Python 中可見。 在此示例中,您傳遞了三個參數(shù):
- cpp_function 是您將在 Python 中使用的函數(shù)的導(dǎo)出名稱。 如本例所示,它不需要匹配 C++ 函數(shù)的名稱。
- &cppmult 使用要導(dǎo)出的函數(shù)的地址。
- "A function..." 是該函數(shù)的可選文檔字符串。
既然您已經(jīng)有了 Python 綁定的代碼,請看一下如何將其構(gòu)建到 Python 模塊中。
構(gòu)建 python 綁定
在 PyBind11 中用于構(gòu)建 Python 綁定的工具是 C++ 編譯器本身。您可能需要修改編譯器和操作系統(tǒng)的默認設(shè)置。
首先,您必須構(gòu)建要為其創(chuàng)建綁定的 C++ 庫。 例如,您可以將 cppmult 庫直接構(gòu)建到 Python 綁定庫中。 但是,對于大多數(shù)實際示例,您將擁有一個要包裝的預(yù)先存在的庫,因此您將單獨構(gòu)建 cppmult 庫。 該構(gòu)建是對編譯器的一個標(biāo)準(zhǔn)調(diào)用,以構(gòu)建一個共享庫:
# 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 綁定的構(gòu)建需要一些特殊的細節(jié):
# 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 行包含相當(dāng)標(biāo)準(zhǔn)的 C++ 編譯器標(biāo)志,這些標(biāo)志指示一些詳細信息,包括您希望捕獲所有警告并將其視為錯誤,需要共享庫以及正在使用 C++11。
第 4 行是魔術(shù)的第一步。它調(diào)用 pybind11 模塊,使其為 PyBind11 生成正確的包含路徑。您可以直接在控制臺上運行此命令以查看其作用:
$ python3 -m pybind11 --includes -I/home/jima/.virtualenvs/realpython/include/python3.7m -I/home/jima/.virtualenvs/realpython/include/site/python3.7您的輸出應(yīng)該相似,但顯示不同的路徑。
在編譯調(diào)用的第 5 行中,您可以看到您還在將路徑添加到 Python 開發(fā)人員包含的文件中。 建議您不要鏈接到 Python 庫本身,但是源代碼需要 Python.h 中的一些代碼才能發(fā)揮其魔力。 幸運的是,它使用的代碼在 Python 版本之間相當(dāng)穩(wěn)定。
第5行還使用 -I. 將當(dāng)前目錄添加到包含路徑列表中。這樣可以解析包裝代碼中的 #include <cppmult.hpp>行。
第 6 行指定源文件的名稱,即 pybind11_wrapper.cpp。 然后,在第 7 行上,您將看到更多的構(gòu)建魔術(shù)正在發(fā)生。 此行指定輸出文件的名稱。 Python 在模塊命名方面有一些特別的想法,包括 Python 版本,機器架構(gòu)和其他細節(jié)。 Python 還提供了一個工具來幫助解決這個問題,稱為 python3.7-config:
$ python3.7-config --extension-suffix .cpython-37m-x86_64-linux-gnu.so如果您使用的是其他版本的 Python,則可能需要修改命令。 如果您使用其他版本的 Python 或使用其他操作系統(tǒng),則結(jié)果可能會發(fā)生變化。
構(gòu)建命令的最后一行(第 8 行)將鏈接器指向您之前構(gòu)建的 libcppmult 庫。 rpath 部分告訴鏈接器將信息添加到共享庫,以幫助操作系統(tǒng)在運行時找到 libcppmult。 最后,您會注意到該字符串的格式為 cpp_name 和 extension_name。 在下一部分中使用 Cython 構(gòu)建 Python 綁定模塊時,將再次使用此功能。
$ invoke build-pybind11 ================================================== = Building C++ Library * Complete ================================================== = Building PyBind11 Module * Complete是的!您已經(jīng)使用 PyBind11 構(gòu)建了 Python 綁定。現(xiàn)在該進行測試了!
調(diào)用你編寫的方法
與上面的 CFFI 示例類似,完成創(chuàng)建 Python 綁定的繁重工作后,調(diào)用函數(shù)看起來就像普通的 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 作為模塊的名稱,因此,這就是您導(dǎo)入的名稱。 在 m.def() 調(diào)用中,您告訴 PyBind11 將 cppmult 函數(shù)導(dǎo)出為 cpp_function,這就是從 Python 調(diào)用它的方法。
您也可以使用 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++ 庫非常有吸引力:
- 它支持類。
- 它處理多態(tài)子類化。
- 它使您可以從 Python 和許多其他工具向?qū)ο筇砑觿討B(tài)屬性,而這對于您已經(jīng)研究過的基于 C 的工具來說是很難做到的。
話雖這么說,您需要做一些設(shè)置和配置來啟動和運行 PyBind11。 正確安裝和構(gòu)建可能有些棘手,但是一旦完成,它似乎就很牢固了。 另外,PyBind11 要求您至少使用 C++11 或更高版本。 對于大多數(shù)項目而言,這不太可能成為一個很大的限制,但您可能需要考慮。
最后,創(chuàng)建 Python 綁定所需編寫的額外代碼是 C++,而不是 Python。 這可能對您來說不是問題,但與您在此處看到的跟其他工具不同。 在下一節(jié)中,您將繼續(xù)學(xué)習(xí) Cython,它采用了完全不同的方法來解決此問題。
Cython
Cython 用于創(chuàng)建 Python 綁定的方法,使用類似于 Python 的語言來定義綁定,然后生成可編譯到模塊中的 C 或 C++ 代碼。 有多種使用 Cython 構(gòu)建 Python 綁定的方法。 最常見的一種是使用 distutils 中的安裝程序。 在此示例中,您將繼續(xù)使用 invoke 工具,該工具將使您能夠精確執(zhí)行所運行的命令。
如何安裝
Cython 是一個 Python 模塊,可以從 PyPI 安裝到您的虛擬環(huán)境中:
$ python3 -m pip install cython同樣,如果您已經(jīng)將 requirements.txt 文件安裝到虛擬環(huán)境中,則該文件已經(jīng)存在。您可以通過單擊鏈接來獲取requirements.txt 的副本。
那應(yīng)該已經(jīng)準(zhǔn)備好與 Cython 合作!
調(diào)用方法
要使用 Cython 構(gòu)建 Python 綁定,您將遵循與用于 CFFI 和 PyBind11 的步驟相似的步驟。 您將編寫綁定,構(gòu)建它們,然后運行 Python 代碼來調(diào)用它們。 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 行創(chuàng)建了包裝函數(shù) pymult(),以調(diào)用 cppmult()。
這里使用的語言是 C,C++ 和 Python 的特殊組合。 不過,對于 Python 開發(fā)人員來說,它看起來相當(dāng)熟悉,因為其目標(biāo)是使過程變得更容易。
使用 cdef extern... 的第一部分告訴 Cython,以下函數(shù)聲明也在 cppmult.hpp 文件中找到。 這對于確保針對與 C++ 代碼相同的聲明構(gòu)建 Python 綁定很有用。 第二部分看起來像是常規(guī)的 Python 函數(shù)-因為它是! 本部分創(chuàng)建可訪問 C++ 函數(shù) cppmult 的 Python 函數(shù)。
現(xiàn)在您已經(jīng)定義了 Python 綁定,是時候構(gòu)建它們了!
構(gòu)建 python 綁定
Cython 的構(gòu)建過程與您用于 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 工具生成額外包含路徑的調(diào)用仍在該函數(shù)中。 在這里不會造成任何傷害,因為您的源碼將不需要這些。
在 invoke 中運行此任務(wù)將產(chǎn)生以下輸出:
$ invoke build-cython ================================================== = Building C++ Library * Complete ================================================== = Building Cython Module * Complete您會看到它構(gòu)建了 cppmult 庫,然后構(gòu)建了 cython 模塊來包裝它。現(xiàn)在您有了 Cython Python 綁定。(嘗試快速地說出……),現(xiàn)在該進行測試了!
調(diào)用你編寫的方法
調(diào)用新的 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 行將導(dǎo)入新的 Python 綁定模塊,并在第 7 行調(diào)用 pymult()。請記住,.pyx 文件在 cppmult() 周圍提供了 Python 包裝器,并將其重命名為 pymult。使用 invoke 運行測試將產(chǎn)生以下結(jié)果:
$ 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您得到與以前相同的結(jié)果!
長處和短處
Cython 是一個相對復(fù)雜的工具,在為 C 或 C++ 創(chuàng)建 Python 綁定時,可以為您提供更深層次的控制。 盡管這里沒有詳細介紹,但它提供了一種 Python 風(fēng)格的方法來編寫可手動控制 GIL 的代碼,從而可以顯著加快某些類型的問題。
但是,這種 Python 風(fēng)格的語言并不完全是 Python,因此,當(dāng)您逐漸確定 C 和 Python 的哪些部分適合您時,會有一條學(xué)習(xí)曲線。
其它一些解決方案
在研究本教程時,我遇到了幾種用于創(chuàng)建 Python 綁定的工具和選項。 盡管我將本概述限制為一些更常見的選項,但我偶然發(fā)現(xiàn)了其他幾種工具。 以下列表并不全面。 如果上述工具之一不適合您的項目,則僅是其他可能性的示例。
PyBindGen
PyBindGen 生成用于 C 或 C++ 的 Python 綁定,并用 Python 編寫。 它的目標(biāo)是產(chǎn)生可讀的 C 或 C++ 代碼,這將簡化調(diào)試問題。 目前尚不清楚是否最近更新,因為該文檔將 Python 3.4 列為最新測試版本。 但是,最近幾年每年都有版本發(fā)布。
Boost.Python
Boost.Python 具有類似于您在上面看到的 PyBind11 的接口。 這不是巧合,因為 PyBind11 是基于該庫的! Boost.Python 是用完整的 C++ 編寫的,并且在大多數(shù)平臺上支持大多數(shù)(如果不是全部)C++ 版本。 相比之下,PyBind11 僅限于使用現(xiàn)代 C++。
SIP
SIP 是為 PyQt 項目開發(fā)的用于生成 Python 綁定的工具集。 wxPython 項目也使用它來生成其綁定。 它具有代碼生成工具和一個額外的 Python 模塊,該模塊為生成的代碼提供支持功能。
Cppyy
cppyy 是一個有趣的工具,其設(shè)計目標(biāo)與到目前為止所看到的略有不同。用包作者的話來說:
“cppyy 的原始想法(可追溯到2001年)是允許生活在 C++ 世界中的 Python 程序員訪問那些 C++ 程序包,而不必直接接觸 C++(或等待 C++ 開發(fā)人員到來并提供綁定)。” 原始鏈接Shiboken
Shiboken 是一種生成 Python 綁定的工具,該工具是為與 Qt 項目關(guān)聯(lián)的 PySide 項目開發(fā)的。 雖然將其設(shè)計為該項目的工具,但文檔顯示它既不是 Qt 也不是 PySide專 用的,并且可以用于其他項目。
SWIG
SWIG 是與此處列出的任何其他工具不同的工具。 這是一種通用工具,可用于創(chuàng)建與許多其他語言(不僅限于 Python)的 C 和 C++ 程序的綁定。 在某些項目中,這種為不同語言生成綁定的功能可能會非常有用。 就復(fù)雜性而言,當(dāng)然要付出代價。
結(jié)論
恭喜! 現(xiàn)在,您已經(jīng)對用于創(chuàng)建 Python 綁定的幾種不同選項有了一定的概覽。 您已經(jīng)了解了編組數(shù)據(jù)和創(chuàng)建綁定時需要考慮的問題。 您已經(jīng)了解了使用以下工具可以從 Python 調(diào)用 C 或 C++ 函數(shù)的過程:
- ctypes
- CFFI
- PyBind11
- Cython
您現(xiàn)在已經(jīng)知道,盡管 ctypes 允許您直接加載 DLL 或共享庫,但其他三個工具需要額外的步驟,但仍會創(chuàng)建完整的 Python 模塊。 另外,您還可以使用 invoke 工具從 Python 側(cè)運行命令行任務(wù)。 單擊鏈接可以獲取在本教程中看到的所有代碼。
現(xiàn)在選擇您喜歡的工具并開始構(gòu)建這些 Python 綁定吧!特別感謝 Loic Domaigne 對本教程的額外技術(shù)評論。
總結(jié)
以上是生活随笔為你收集整理的c++ 内存管理_Python Bindings - 从 Python 调用 C/C++的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql cmd 实时监控_MySQL
- 下一篇: c++ 使用nacos_为什么选用Nac