賬號(hào)為華為云開發(fā)者社區(qū)官方運(yùn)營賬號(hào),提供全面深入的云計(jì)算前景分析、豐富的技術(shù)干貨、程序樣例,分享華為云前沿資訊動(dòng)態(tài)
本文分享自華為云社區(qū)《Python 綁定:從 Python 調(diào)用 C 或 C++ |【生長吧!Python!】》,原文作者:Yuchuan 。
您是擁有想要從 Python 中使用的C或 C++ 庫的 Python 開發(fā)人員嗎?如果是這樣,那么Python 綁定允許您調(diào)用函數(shù)并將數(shù)據(jù)從 Python 傳遞到C或C++,讓您利用這兩種語言的優(yōu)勢(shì)。在本教程中,您將看到一些可用于創(chuàng)建 Python 綁定的工具的概述。
在本教程中,您將了解:
本教程面向中級(jí) Python 開發(fā)人員。它假定讀者具備 Python 的基本知識(shí),并對(duì) C 或 C++ 中的函數(shù)和數(shù)據(jù)類型有所了解。您可以通過單擊下面的鏈接獲取本教程中將看到的所有示例代碼:
在深入研究如何從 Python 調(diào)用 C之前,最好花一些時(shí)間了解為什么. 有幾種情況下,創(chuàng)建 Python 綁定來調(diào)用 C 庫是一個(gè)好主意:
以上所有都是學(xué)習(xí)創(chuàng)建 Python 綁定以與 C 庫交互的重要原因。
注意:在本教程中,您將創(chuàng)建到 C和C++ 的Python 綁定。大多數(shù)通用概念適用于兩種語言,因此除非兩種語言之間存在特定差異,否則將使用 C。通常,每個(gè)工具都支持 C或C++,但不能同時(shí)支持兩者。
讓我們開始吧!
等待!在開始編寫 Python 綁定之前,先看看 Python 和 C 如何存儲(chǔ)數(shù)據(jù)以及這會(huì)導(dǎo)致哪些類型的問題。首先,讓我們定義編組。這個(gè)概念由維基百科定義如下:
將對(duì)象的內(nèi)存表示轉(zhuǎn)換為適合存儲(chǔ)或傳輸?shù)臄?shù)據(jù)格式的過程。
出于您的目的,編組是 Python 綁定在準(zhǔn)備數(shù)據(jù)以將其從 Python 移動(dòng)到 C 或反之亦然時(shí)所做的工作。Python 綁定需要進(jìn)行編組,因?yàn)?Python 和 C 以不同的方式存儲(chǔ)數(shù)據(jù)。C 在內(nèi)存中以最緊湊的形式存儲(chǔ)數(shù)據(jù)。如果您使用uint8_t,那么它總共將只使用 8 位內(nèi)存。
另一方面,在 Python 中,一切都是對(duì)象。這意味著每個(gè)整數(shù)在內(nèi)存中使用幾個(gè)字節(jié)。多少取決于您運(yùn)行的 Python 版本、操作系統(tǒng)和其他因素。這意味著您的 Python 綁定將需要為每個(gè)跨邊界傳遞的整數(shù)將C 整數(shù)轉(zhuǎn)換為Python 整數(shù)。
其他數(shù)據(jù)類型在這兩種語言之間具有相似的關(guān)系。讓我們依次來看看:
除了數(shù)據(jù)類型轉(zhuǎn)換之外,在構(gòu)建 Python 綁定時(shí)還需要考慮其他問題。讓我們繼續(xù)探索它們。
除了所有這些數(shù)據(jù)類型之外,您還必須了解 Python 對(duì)象如何可變或不可變。當(dāng)談到傳值或傳引用時(shí),C 有一個(gè)類似的函數(shù)參數(shù)概念。在 C 中,所有參數(shù)都是按值傳遞的。如果要允許函數(shù)更改調(diào)用方中的變量,則需要傳遞指向該變量的指針。
您可能想知道是否可以通過使用指針將不可變對(duì)象簡單地傳遞給 C 來繞過不可變限制。除非你走到丑陋和不可移植的極端,否則Python 不會(huì)給你一個(gè)指向 object 的指針,所以這行不通。如果您想用 C 修改 Python 對(duì)象,那么您需要采取額外的步驟來實(shí)現(xiàn)這一點(diǎn)。這些步驟將取決于您使用的工具,如下所示。
因此,您可以將不變性添加到您創(chuàng)建 Python 綁定時(shí)要考慮的項(xiàng)目清單中。在創(chuàng)建此清單的宏偉之旅中,您的最后一站是如何處理 Python 和 C 處理內(nèi)存管理的不同方式。
C 和 Python管理內(nèi)存的方式不同。在 C 中,開發(fā)人員必須管理所有內(nèi)存分配并確保它們被釋放一次且僅一次。Python 使用垃圾收集器為您處理這個(gè)問題。
雖然這些方法中的每一種都有其優(yōu)點(diǎn),但它確實(shí)為創(chuàng)建 Python 綁定添加了額外的麻煩。您需要知道每個(gè)對(duì)象的內(nèi)存分配在哪里,并確保它只在語言障礙的同一側(cè)被釋放。
例如,當(dāng)您設(shè)置x=3. 用于此的內(nèi)存在 Python 端分配,需要進(jìn)行垃圾收集。幸運(yùn)的是,使用 Python 對(duì)象,很難做任何其他事情。看看 C 中的逆向,直接分配一塊內(nèi)存:
int* iPtr=(int*)malloc(sizeof(int));
執(zhí)行此操作時(shí),您需要確保在 C 中釋放此指針。這可能意味著手動(dòng)將代碼添加到 Python 綁定中以執(zhí)行此操作。
這完善了您的一般主題清單。讓我們開始設(shè)置您的系統(tǒng),以便您可以編寫一些代碼!
在本教程中,您將使用來自 Real Python GitHub 存儲(chǔ)庫的預(yù)先存在的 C 和 C++ 庫來展示每個(gè)工具的測試。目的是您將能夠?qū)⑦@些想法用于任何 C 庫。要遵循此處的所有示例,您需要具備以下條件:
最后一個(gè)對(duì)你來說可能是新的,所以讓我們仔細(xì)看看它。
invoke是您將在本教程中用于構(gòu)建和測試 Python 綁定的工具。它具有類似的目的,make但使用 Python 而不是 Makefiles。您需要invoke使用pip以下命令在虛擬環(huán)境中安裝:
$ python3 -m pip install invoke
要運(yùn)行它,請(qǐng)鍵入invoke后跟要執(zhí)行的任務(wù):
$ invoke build-cmult===================================================Building C Library
* Complete
要查看哪些任務(wù)可用,請(qǐng)使用以下--list選項(xiàng):
$ invoke --list
Available tasks:
all Build and run all tests
build-cffi Build the CFFI Python bindings
build-cmult Build the shared library for the sample C code
build-cppmult Build the shared library for the sample C++ code
build-cython Build the cython extension module
build-pybind11 Build the pybind11 wrapper library
clean Remove any built objects
test-cffi Run the script to test CFFI
test-ctypes Run the script to test ctypes
test-cython Run the script to test Cython
test-pybind11 Run the script to test PyBind11
請(qǐng)注意,當(dāng)您查看定義任務(wù)的tasks.py文件時(shí)invoke,您會(huì)看到列出的第二個(gè)任務(wù)的名稱是build_cffi. 但是,來自的輸出將其--list顯示為build-cffi. 減號(hào) ( -) 不能用作 Python 名稱的一部分,因此該文件使用下劃線 ( _) 代替。
對(duì)于您將檢查的每個(gè)工具,都會(huì)定義一個(gè)build-和一個(gè)test-任務(wù)。例如,要運(yùn)行 的代碼CFFI,您可以鍵入invoke build-cffi test-cffi。一個(gè)例外是ctypes,因?yàn)?沒有構(gòu)建階段ctypes。此外,為了方便,還添加了兩個(gè)特殊任務(wù):
既然您已經(jīng)對(duì)如何運(yùn)行代碼有所了解,那么在查看工具概述之前,讓我們先看一下您將要包裝的 C 代碼。
在下面的每個(gè)示例部分中,您將為C 或 C++ 中的相同函數(shù)創(chuàng)建 Python 綁定。這些部分旨在讓您體驗(yàn)每種方法的外觀,而不是有關(guān)該工具的深入教程,因此您將封裝的函數(shù)很小。您將為其創(chuàng)建 Python 綁定的函數(shù)將 anint和 afloat作為輸入?yún)?shù)并返回一個(gè)float是兩個(gè)數(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 %.1f\n", int_param,
float_param, return_value);
return return_value;
}
C 和 C++ 函數(shù)幾乎相同,它們之間的名稱和字符串略有不同。您可以通過單擊以下鏈接獲取所有代碼的副本:
現(xiàn)在您已經(jīng)克隆了 repo 并安裝了工具,您可以構(gòu)建和測試這些工具。因此,讓我們深入了解下面的每個(gè)部分!
您將從 開始ctypes,它是標(biāo)準(zhǔn)庫中用于創(chuàng)建 Python 綁定的工具。它提供了一個(gè)低級(jí)工具集,用于在 Python 和 C 之間加載共享庫和編組數(shù)據(jù)。
的一大優(yōu)點(diǎn)ctypes是它是 Python 標(biāo)準(zhǔn)庫的一部分。它是在 Python 2.5 版中添加的,因此您很可能已經(jīng)擁有它。您可以import像使用sys或time模塊一樣。
加載 C 庫和調(diào)用函數(shù)的所有代碼都將在 Python 程序中。這很棒,因?yàn)槟倪^程中沒有額外的步驟。您只需運(yùn)行您的程序,一切都會(huì)得到處理。要在 中創(chuàng)建 Python 綁定ctypes,您需要執(zhí)行以下步驟:
您將依次查看其中的每一個(gè)。
ctypes為您提供了多種加載共享庫的方法,其中一些是特定于平臺(tái)的。對(duì)于您的示例,您將ctypes.CDLL通過傳入所需共享庫的完整路徑來直接創(chuàng)建對(duì)象:
# ctypes_test.py
import ctypes
import pathlib
if __name__=="__main__":
# Load the shared library into ctypes
libname=pathlib.Path().absolute() / "libcmult.so"
c_lib=ctypes.CDLL(libname)
這適用于共享庫與 Python 腳本位于同一目錄中的情況,但在嘗試加載來自 Python 綁定以外的包的庫時(shí)要小心。在ctypes特定于平臺(tái)和特定情況的文檔中,有許多關(guān)于加載庫和查找路徑的詳細(xì)信息。
注意:在庫加載過程中可能會(huì)出現(xiàn)許多特定于平臺(tái)的問題。最好在示例工作后進(jìn)行增量更改。
現(xiàn)在您已將庫加載到 Python 中,您可以嘗試調(diào)用它!
請(qǐng)記住,您的 C 函數(shù)的函數(shù)原型如下:
// cmult.h
float cmult(int int_param, float float_param);
您需要傳入一個(gè)整數(shù)和一個(gè)浮點(diǎn)數(shù),并且可以期望得到一個(gè)浮點(diǎn)數(shù)返回。整數(shù)和浮點(diǎn)數(shù)在 Python 和 C 中都有本機(jī)支持,因此您希望這種情況適用于合理的值。
將庫加載到 Python 綁定中后,該函數(shù)將成為 的屬性c_lib,即CDLL您之前創(chuàng)建的對(duì)象。您可以嘗試這樣稱呼它:
x, y=6, 2.3
answer=c_lib.cmult(x, y)
哎呀!這不起作用。此行在示例 repo 中被注釋掉,因?yàn)樗×?。如果您嘗試使用該調(diào)用運(yùn)行,那么 Python 會(huì)報(bào)錯(cuò):
$ 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除非您明確告訴它,否則您對(duì)該函數(shù)一無所知。任何未以其他方式標(biāo)記的參數(shù)都假定為整數(shù)。ctypes不知道如何將2.3存儲(chǔ)的值轉(zhuǎn)換為y整數(shù),所以它失敗了。
要解決此問題,您需要c_float從號(hào)碼中創(chuàng)建一個(gè)。您可以在調(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)您運(yùn)行此代碼時(shí),它會(huì)返回您傳入的兩個(gè)數(shù)字的乘積:
$ invoke test-ctypes
In cmult : int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 48.0
等一下……6乘以2.3不是48.0!
事實(shí)證明,就像輸入?yún)?shù)一樣,ctypes 假設(shè)您的函數(shù)返回一個(gè)int. 實(shí)際上,您的函數(shù)返回 a float,它被錯(cuò)誤地編組。就像輸入?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)該夠了吧。讓我們運(yùn)行整個(gè)test-ctypes目標(biāo),看看你有什么。請(qǐng)記住,輸出的第一部分是在restype將函數(shù)固定為浮點(diǎn)數(shù)之前:
$ invoke test-ctypes===================================================Building C Library
* Complete===================================================Testing ctypes Module
In cmult : int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 48.0
In cmult : int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 13.8
這樣更好!雖然第一個(gè)未更正的版本返回錯(cuò)誤的值,但您的固定版本與 C 函數(shù)一致。C 和 Python 都得到相同的結(jié)果!現(xiàn)在它可以工作了,看看為什么您可能想或不想使用ctypes.
ctypes與您將在此處檢查的其他工具相比,最大的優(yōu)勢(shì)在于它內(nèi)置于標(biāo)準(zhǔn)庫中。它還不需要額外的步驟,因?yàn)樗泄ぷ鞫际亲鳛?Python 程序的一部分完成的。
此外,所使用的概念是低級(jí)的,這使得像您剛剛做的那樣的練習(xí)易于管理。然而,由于缺乏自動(dòng)化,更復(fù)雜的任務(wù)變得繁瑣。在下一部分中,您將看到一個(gè)工具,該工具為流程添加了一些自動(dòng)化。
CFFI是Python的C 外來函數(shù)接口。生成 Python 綁定需要更自動(dòng)化的方法。CFFI有多種方式可以構(gòu)建和使用 Python 綁定。有兩種不同的選項(xiàng)可供選擇,為您提供四種可能的模式:
對(duì)于此示例,您將使用 API 外聯(lián)模式,它生成最快的代碼,并且通??雌饋眍愃朴谀鷮⒃诒窘坛毯竺鎰?chuàng)建的其他 Python 綁定。
由于CFFI不是標(biāo)準(zhǔn)庫的一部分,您需要在您的機(jī)器上安裝它。建議您為此創(chuàng)建一個(gè)虛擬環(huán)境。幸運(yùn)的是,CFFI安裝有pip:
$ python3 -m pip install cffi
這會(huì)將軟件包安裝到您的虛擬環(huán)境中。如果您已經(jīng)從 安裝requirements.txt,那么應(yīng)該注意這一點(diǎn)。您可以requirements.txt通過訪問以下鏈接中的 repo 來查看:
獲取示例代碼: 單擊此處獲取您將用于在本教程中了解 Python 綁定的示例代碼。
現(xiàn)在你已經(jīng)CFFI安裝好了,是時(shí)候試一試了!
與 不同的是ctypes,CFFI您正在創(chuàng)建一個(gè)完整的 Python 模塊。您將能夠import像標(biāo)準(zhǔn)庫中的任何其他模塊一樣使用該模塊。您需要做一些額外的工作來構(gòu)建 Python 模塊。要使用CFFIPython 綁定,您需要執(zhí)行以下步驟:
這可能看起來需要做很多工作,但您將完成這些步驟中的每一個(gè),并了解它是如何工作的。
CFFI提供讀取C 頭文件的方法,以在生成 Python 綁定時(shí)完成大部分工作。在 的文檔中CFFI,執(zhí)行此操作的代碼放置在單獨(dú)的 Python 文件中。對(duì)于此示例,您將直接將該代碼放入構(gòu)建工具中invoke,該工具使用 Python 文件作為輸入。要使用CFFI,您首先要?jiǎng)?chuàng)建一個(gè)cffi.FFI對(duì)象,該對(duì)象提供了您需要的三種方法:
# tasks.py
import cffi
...
""" Build the CFFI Python bindings """
print_banner("Building CFFI Module")
ffi=cffi.FFI()
擁有 FFI 后,您將使用.cdef()來自動(dòng)處理頭文件的內(nèi)容。這會(huì)為您創(chuàng)建包裝函數(shù)以從 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ù)的細(xì)分:
調(diào)用.set_source()不會(huì)構(gòu)建 Python 綁定。它只設(shè)置元數(shù)據(jù)來描述將生成的內(nèi)容。要構(gòu)建 Python 綁定,您需要調(diào)用.compile():
# tasks.py
ffi.compile()
這通過生成.c文件、.o文件和共享庫來完成。在invoke你剛走通過任務(wù)可以在上運(yùn)行命令行構(gòu)建Python綁定:
$ invoke build-cffi===================================================Building C Library
* Complete===================================================Building CFFI Module
* Complete
你有你的CFFIPython 綁定,所以是時(shí)候運(yùn)行這段代碼了!
在您為配置和運(yùn)行CFFI編譯器所做的所有工作之后,使用生成的 Python 綁定看起來就像使用任何其他 Python 模塊一樣:
# cffi_test.py
import cffi_example
if __name__=="__main__":
# Sample data for your call
x, y=6, 2.3
answer=cffi_example.lib.cmult(x, y)
print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")
你導(dǎo)入新模塊,然后就可以cmult()直接調(diào)用了。要對(duì)其進(jìn)行測試,請(qǐng)使用以下test-cffi任務(wù):
$ invoke test-cffi===================================================Testing CFFI Module
In cmult : int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 13.8
這將運(yùn)行您的cffi_test.py程序,該程序會(huì)測試您使用CFFI. 關(guān)于編寫和使用CFFIPython 綁定的部分到此結(jié)束。
ctypes與CFFI您剛剛看到的示例相比,這似乎需要更少的工作。雖然這對(duì)于這個(gè)用例來說是正確的,CFFI但與ctypes由于大部分功能包裝的自動(dòng)化相比,它可以更好地?cái)U(kuò)展到更大的項(xiàng)目。
CFFI也產(chǎn)生了完全不同的用戶體驗(yàn)。ctypes允許您將預(yù)先存在的 C 庫直接加載到您的 Python 程序中。CFFI,另一方面,創(chuàng)建一個(gè)可以像其他 Python 模塊一樣加載的新 Python 模塊。
更重要的是,使用上面使用的外部 API方法,創(chuàng)建 Python 綁定的時(shí)間損失在您構(gòu)建它時(shí)完成一次,并且不會(huì)在每次運(yùn)行代碼時(shí)發(fā)生。對(duì)于小程序來說,這可能不是什么大問題,但也可以通過CFFI這種方式更好地?cái)U(kuò)展到更大的項(xiàng)目。
就像ctypes, usingCFFI只允許您直接與 C 庫交互。C++ 庫需要大量的工作才能使用。在下一節(jié)中,您將看到一個(gè)專注于 C++ 的 Python 綁定工具。
PyBind11使用完全不同的方法來創(chuàng)建 Python 綁定。除了將重點(diǎn)從 C 轉(zhuǎn)移到 C++ 之外,它還使用 C++ 來指定和構(gòu)建模塊,使其能夠利用 C++ 中的元編程工具。像 一樣CFFI,生成的 Python 綁定PyBind11是一個(gè)完整的 Python 模塊,可以直接導(dǎo)入和使用。
PyBind11以Boost::Python庫為藍(lán)本并具有類似的界面。但是,它將其使用限制為 C++11 和更新版本,與支持所有內(nèi)容的 Boost 相比,這使其能夠簡化和加快處理速度。
文檔的“第一步”部分將PyBind11引導(dǎo)您了解如何下載和構(gòu)建PyBind11. 雖然這似乎不是嚴(yán)格要求,但完成這些步驟將確保您設(shè)置了正確的 C++ 和 Python 工具。
注:大部分示例PyBind11使用cmake,是構(gòu)建 C 和 C++ 項(xiàng)目的好工具。但是,對(duì)于此演示,您將繼續(xù)使用該invoke工具,該工具遵循文檔的手動(dòng)構(gòu)建部分中的說明。
您需要將此工具安裝到您的虛擬環(huán)境中:
$ python3 -m pip install pybind11
PyBind11是一個(gè)全頭庫,類似于 Boost 的大部分內(nèi)容。這允許pip將庫的實(shí)際 C++ 源代碼直接安裝到您的虛擬環(huán)境中。
在您深入研究之前,請(qǐng)注意您使用的是不同的 C++ 源文件, cppmult.cpp,而不是您用于前面示例的 C 文件。兩種語言的功能基本相同。
與 類似CFFI,您需要?jiǎng)?chuàng)建一些代碼來告訴該工具如何構(gòu)建您的 Python 綁定。與 不同CFFI,此代碼將使用 C++ 而不是 Python。幸運(yùn)的是,只需要很少的代碼:
// pybind11_wrapper.cpp
#include <pybind11/pybind11.h>
#include <cppmult.hpp>
PYBIND11_MODULE(pybind11_example, m) {
m.doc()="pybind11 example plugin"; // Optional module docstring
m.def("cpp_function", &cppmult, "A function that multiplies two numbers");
}
讓我們一次一個(gè)地看,因?yàn)镻yBind11將大量信息打包成幾行。
前兩行包括pybind11.hC++ 庫的文件和頭文件cppmult.hpp. 之后,你就有了PYBIND11_MODULE宏。這將擴(kuò)展為PyBind11源代碼中詳細(xì)描述的 C++ 代碼塊:
此宏創(chuàng)建入口點(diǎn),當(dāng) Python 解釋器導(dǎo)入擴(kuò)展模塊時(shí)將調(diào)用該入口點(diǎn)。模塊名稱作為第一個(gè)參數(shù)給出,不應(yīng)用引號(hào)引起來。第二個(gè)宏參數(shù)定義了一個(gè)py::module可用于初始化模塊的類型變量。(來源)
這對(duì)您來說意味著,在本例中,您正在創(chuàng)建一個(gè)名為的模塊pybind11_example,其余代碼將m用作py::module對(duì)象的名稱。在下一行,在您定義的 C++ 函數(shù)中,您為模塊創(chuàng)建一個(gè)文檔字符串。雖然這是可選的,但讓您的模塊更加Pythonic是一個(gè)不錯(cuò)的選擇。
最后,你有m.def()電話。這將定義一個(gè)由您的新 Python 綁定導(dǎo)出的函數(shù),這意味著它將在 Python 中可見。在此示例中,您將傳遞三個(gè)參數(shù):
現(xiàn)在您已經(jīng)有了 Python 綁定的代碼,接下來看看如何將其構(gòu)建到 Python 模塊中。
用于構(gòu)建 Python 綁定的工具PyBind11是 C++ 編譯器本身。您可能需要修改編譯器和操作系統(tǒng)的默認(rèn)值。
首先,您必須構(gòu)建要為其創(chuàng)建綁定的 C++ 庫。對(duì)于這么小的示例,您可以將cppmult庫直接構(gòu)建到 Python 綁定庫中。但是,對(duì)于大多數(shù)實(shí)際示例,您將有一個(gè)要包裝的預(yù)先存在的庫,因此您將cppmult單獨(dú)構(gòu)建該庫。構(gòu)建是對(duì)編譯器的標(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 "
)
運(yùn)行這個(gè)invoke build-cppmult產(chǎn)生libcppmult.so:
$ invoke build-cppmult===================================================Building C++ Library
* Complete
另一方面,Python 綁定的構(gòu)建需要一些特殊的細(xì)節(jié):
1# tasks.py
2invoke.run(
3 "g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC "
4 "`python3 -m pybind11 --includes` "
5 "-I /usr/include/python3.7 -I . "
6 "{0} "
7 "-o {1}`python3.7-config --extension-suffix` "
8 "-L. -lcppmult -Wl,-rpath,.".format(cpp_name, extension_name)
9)
讓我們逐行瀏覽一下。第 3 行包含相當(dāng)標(biāo)準(zhǔn)的 C++ 編譯器標(biāo)志,指示幾個(gè)細(xì)節(jié),包括您希望捕獲所有警告并將其視為錯(cuò)誤、您需要共享庫以及您使用的是 C++11。
第 4 行是魔法的第一步。它調(diào)用pybind11模塊使其include為PyBind11. 您可以直接在控制臺(tái)上運(yùn)行此命令以查看它的作用:
$ 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 dev 的路徑includes。雖然建議您不要鏈接 Python 庫本身,但源代碼需要一些代碼Python.h才能發(fā)揮其魔力。幸運(yùn)的是,它使用的代碼在 Python 版本中相當(dāng)穩(wěn)定。
第 5 行還用于-I .將當(dāng)前目錄添加到include路徑列表中。這允許#include <cppmult.hpp>解析包裝器代碼中的行。
第 6 行指定源文件的名稱,即pybind11_wrapper.cpp. 然后,在第 7 行,您會(huì)看到更多的構(gòu)建魔法正在發(fā)生。此行指定輸出文件的名稱。Python 在模塊命名上有一些特別的想法,包括 Python 版本、機(jī)器架構(gòu)和其他細(xì)節(jié)。Python 還提供了一個(gè)工具來幫助解決這個(gè)問題python3.7-config:
$ python3.7-config --extension-suffix
.cpython-37m-x86_64-linux-gnu.so
如果您使用的是不同版本的 Python,則可能需要修改該命令。如果您使用不同版本的 Python 或在不同的操作系統(tǒng)上,您的結(jié)果可能會(huì)發(fā)生變化。
構(gòu)建命令的最后一行,第 8 行,將鏈接器指向libcppmult您之前構(gòu)建的庫。該rpath部分告訴鏈接器向共享庫添加信息以幫助操作系統(tǒng)libcppmult在運(yùn)行時(shí)查找。最后,您會(huì)注意到此字符串的格式為cpp_name和extension_name。Cython在下一節(jié)中構(gòu)建 Python 綁定模塊時(shí),您將再次使用此函數(shù)。
運(yùn)行此命令以構(gòu)建綁定:
$ invoke build-pybind11===================================================Building C++ Library
* Complete===================================================Building PyBind11 Module
* Complete
就是這樣!您已經(jīng)使用PyBind11. 是時(shí)候測試一下了!
與CFFI上面的示例類似,一旦您完成了創(chuàng)建 Python 綁定的繁重工作,調(diào)用您的函數(shù)看起來就像普通的 Python 代碼:
# pybind11_test.py
import pybind11_example
if __name__=="__main__":
# Sample data for your call
x, y=6, 2.3
answer=pybind11_example.cpp_function(x, y)
print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")
由于您pybind11_example在PYBIND11_MODULE宏中用作模塊的名稱,因此這就是您導(dǎo)入的名稱。在m.def()您告訴PyBind11將cppmult函數(shù)導(dǎo)出為 的調(diào)用中cpp_function,這就是您用來從 Python 調(diào)用它的方法。
你也可以測試它invoke:
$ invoke test-pybind11===================================================Testing PyBind11 Module
In cppmul: int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 13.8
這就是PyBind11看起來的樣子。接下來,您將了解何時(shí)以及為何PyBind11是適合該工作的工具。
PyBind11專注于 C++ 而不是 C,這使得它不同于ctypes和CFFI。它有幾個(gè)特性使其對(duì) C++ 庫非常有吸引力:
話雖如此,您需要進(jìn)行大量設(shè)置和配置才能PyBind11啟動(dòng)和運(yùn)行。正確安裝和構(gòu)建可能有點(diǎn)挑剔,但一旦完成,它似乎相當(dāng)可靠。此外,PyBind11要求您至少使用 C++11 或更高版本。對(duì)于大多數(shù)項(xiàng)目來說,這不太可能是一個(gè)很大的限制,但它可能是您的一個(gè)考慮因素。
最后,創(chuàng)建 Python 綁定需要編寫的額外代碼是用 C++ 編寫的,而不是用 Python 編寫的。這可能是也可能不是你的問題,但它是比你在這里看到的其他工具不同。在下一節(jié)中,您將繼續(xù)討論Cython,它采用完全不同的方法來解決這個(gè)問題。
該方法Cython需要?jiǎng)?chuàng)建Python綁定使用類Python語言來定義綁定,然后生成的C或C ++代碼可被編譯成模塊。有幾種方法可以使用Cython. 最常見的一種是使用setupfrom distutils。對(duì)于此示例,您將堅(jiān)持使用該invoke工具,它允許您使用運(yùn)行的確切命令。
Cython是一個(gè) Python 模塊,可以從PyPI安裝到您的虛擬環(huán)境中:
$ python3 -m pip install cython
同樣,如果您已將該requirements.txt文件安裝到虛擬環(huán)境中,則該文件已經(jīng)存在。您可以requirements.txt通過單擊以下鏈接獲取副本:
獲取示例代碼: 單擊此處獲取您將用于在本教程中了解 Python 綁定的示例代碼。
這應(yīng)該讓你準(zhǔn)備好與之合作Cython!
要使用 構(gòu)建 Python 綁定Cython,您將遵循與用于CFFI和 的步驟類似的步驟PyBind11。您將編寫綁定、構(gòu)建它們,然后運(yùn)行 ?Python 代碼來調(diào)用它們。Cython可以同時(shí)支持 C 和 C++。對(duì)于本示例,您將使用cppmult您在PyBind11上面的示例中使用的庫。
聲明模塊的最常見形式Cython是使用.pyx文件:
1# cython_example.pyx
2""" Example cython interface definition """
3
4cdef extern from "cppmult.hpp":
5 float cppmult(int int_param, float float_param)
6
7def pymult( int_param, float_param ):
8 return cppmult( int_param, float_param )
這里有兩個(gè)部分:
這里使用的語言是 C、C++ 和 Python 的特殊組合。不過,對(duì)于 Python 開發(fā)人員來說,它看起來相當(dāng)熟悉,因?yàn)槠淠繕?biāo)是使過程更容易。
第一部分 withcdef extern...告訴Cython下面的函數(shù)聲明也可以在cppmult.hpp文件中找到。這對(duì)于確保根據(jù)與 C++ 代碼相同的聲明構(gòu)建 Python 綁定非常有用。第二部分看起來像一個(gè)普通的 Python 函數(shù)——因?yàn)樗?!本?jié)創(chuàng)建一個(gè)可以訪問 C++ 函數(shù)的 Python 函數(shù)cppmult。
現(xiàn)在您已經(jīng)定義了 Python 綁定,是時(shí)候構(gòu)建它們了!
構(gòu)建過程Cython與您使用的構(gòu)建過程相似PyBind11。您首先Cython在.pyx文件上運(yùn)行以生成.cpp文件。完成此操作后,您可以使用用于以下內(nèi)容的相同函數(shù)對(duì)其進(jìn)行編譯PyBind11:
1# tasks.py
2def compile_python_module(cpp_name, extension_name):
3 invoke.run(
4 "g++ -O3 -Wall -Werror -shared -std=c++11 -fPIC "
5 "`python3 -m pybind11 --includes` "
6 "-I /usr/include/python3.7 -I . "
7 "{0} "
8 "-o {1}`python3.7-config --extension-suffix` "
9 "-L. -lcppmult -Wl,-rpath,.".format(cpp_name, extension_name)
10 )
11
12def build_cython(c):
13 """ Build the cython extension module """
14 print_banner("Building Cython Module")
15 # Run cython on the pyx file to create a .cpp file
16 invoke.run("cython --cplus -3 cython_example.pyx -o cython_wrapper.cpp")
17
18 # Compile and link the cython wrapper library
19 compile_python_module("cython_wrapper.cpp", "cython_example")
20 print("* Complete")
您首先運(yùn)行cython您的.pyx文件。您可以在此命令上使用幾個(gè)選項(xiàng):
生成 C++ 文件后,您可以使用 C++ 編譯器生成 Python 綁定,就像您為PyBind11. 請(qǐng)注意,include使用該pybind11工具生成額外路徑的調(diào)用仍在該函數(shù)中。在這里不會(huì)有任何傷害,因?yàn)槟膩碓床恍枰@些。
在 中運(yùn)行此任務(wù)invoke會(huì)產(chǎn)生以下輸出:
$ invoke build-cython===================================================Building C++ Library
* Complete===================================================Building Cython Module
* Complete
可以看到它構(gòu)建了cppmult庫,然后構(gòu)建了cython模塊來包裝它?,F(xiàn)在你有了CythonPython 綁定。(試著說的是迅速...)它的時(shí)間來測試一下吧!
調(diào)用新 Python 綁定的 Python 代碼與用于測試其他模塊的代碼非常相似:
1# cython_test.py
2import cython_example
3
4# Sample data for your call
5x, y=6, 2.3
6
7answer=cython_example.pymult(x, y)
8print(f" In Python: int: {x} float {y:.1f} return val {answer:.1f}")
第 2 行導(dǎo)入新的 Python 綁定模塊,并pymult()在第 7 行調(diào)用。請(qǐng)記住,該.pyx文件提供了一個(gè) Python 包裝器cppmult()并將其重命名為pymult. 使用 invoke 運(yùn)行您的測試會(huì)產(chǎn)生以下結(jié)果:
$ invoke test-cython===================================================Testing Cython Module
In cppmul: int: 6 float 2.3 returning 13.8
In Python: int: 6 float 2.3 return val 13.8
你得到和以前一樣的結(jié)果!
Cython是一個(gè)相對(duì)復(fù)雜的工具,可以在為 C 或 C++ 創(chuàng)建 Python 綁定時(shí)為您提供更深層次的控制。雖然您沒有在此處深入介紹它,但它提供了一種 Python 式的方法來編寫手動(dòng)控制GIL 的代碼,這可以顯著加快某些類型的問題的處理速度。
然而,這種 Python 風(fēng)格的語言并不完全是 Python,因此當(dāng)您要快速確定 C 和 Python 的哪些部分適合何處時(shí),會(huì)有一個(gè)輕微的學(xué)習(xí)曲線。
在研究本教程時(shí),我遇到了幾種用于創(chuàng)建 Python 綁定的不同工具和選項(xiàng)。雖然我將此概述限制為一些更常見的選項(xiàng),但我偶然發(fā)現(xiàn)了其他幾種工具。下面的列表并不全面。如果上述工具之一不適合您的項(xiàng)目,這只是其他可能性的一個(gè)示例。
PyBindGen為 C 或 C++ 生成 Python 綁定并用 Python 編寫。它旨在生成可讀的 C 或 C++ 代碼,這應(yīng)該可以簡化調(diào)試問題。目前尚不清楚這是否最近已更新,因?yàn)槲臋n將 Python 3.4 列為最新的測試版本。然而,在過去的幾年里,每年都有發(fā)布。
Boost.Python有一個(gè)類似于PyBind11您在上面看到的界面。這不是巧合,因?yàn)镻yBind11它基于這個(gè)庫!Boost.Python是用完整的 C++ 編寫的,并且在大多數(shù)平臺(tái)上支持大多數(shù)(如果不是全部)C++ 版本。相比之下,PyBind11僅限于現(xiàn)代 C++。
SIP是為PyQt項(xiàng)目開發(fā)的用于生成 Python 綁定的工具集。wxPython項(xiàng)目也使用它來生成它們的綁定。它有一個(gè)代碼生成工具和一個(gè)額外的 Python 模塊,為生成的代碼提供支持功能。
cppyy是一個(gè)有趣的工具,它的設(shè)計(jì)目標(biāo)與您目前所見略有不同。用包作者的話來說:
“cppyy 背后的最初想法(追溯到 2001 年)是允許生活在 C++ 世界中的 Python 程序員訪問那些 C++ 包,而不必直接接觸 C++(或等待 C++ 開發(fā)人員過來并提供綁定) ?!?(來源)
Shiboken是為與 Qt 項(xiàng)目關(guān)聯(lián)的 PySide 項(xiàng)目開發(fā)的用于生成 Python 綁定的工具。雖然它被設(shè)計(jì)為該項(xiàng)目的工具,但文檔表明它既不是 Qt 也不是 PySide 特定的,可用于其他項(xiàng)目。
SWIG是與此處列出的任何其他工具不同的工具。它是一個(gè)通用工具,用于為許多其他語言(而不僅僅是 Python)創(chuàng)建到 C 和 C++ 程序的綁定。這種為不同語言生成綁定的能力在某些項(xiàng)目中非常有用。當(dāng)然,就復(fù)雜性而言,它會(huì)帶來成本。
恭喜!您現(xiàn)在已經(jīng)大致了解了用于創(chuàng)建Python 綁定的幾個(gè)不同選項(xiàng)。您已經(jīng)了解了編組數(shù)據(jù)以及創(chuàng)建綁定時(shí)需要考慮的問題。您已經(jīng)了解了如何使用以下工具從 Python 調(diào)用 C 或 C++ 函數(shù):
您現(xiàn)在知道,雖然ctypes允許您直接加載 DLL 或共享庫,但其他三個(gè)工具需要額外的步驟,但仍會(huì)創(chuàng)建完整的 Python 模塊。作為獎(jiǎng)勵(lì),您還使用了invoke從 Python 運(yùn)行命令行任務(wù)的工具。
點(diǎn)擊關(guān)注,第一時(shí)間了解華為云新鮮技術(shù)~華為云博客_大數(shù)據(jù)博客_AI博客_云計(jì)算博客_開發(fā)者中心-華為云
由于C和C++代碼在編譯時(shí)生成的符號(hào)不同,而我們經(jīng)常會(huì)在C代碼里調(diào)用C++的代碼,
或者在C++代碼里調(diào)用C的代碼,下面就簡單總結(jié)一下二者相互調(diào)用時(shí)的語法。
最主要的就是在C++代碼里添加 extern “C”
1、首先C代碼調(diào)用C++
main.c
#include<stdio.h>
int sum(int a,int b);//此時(shí)sum函數(shù)生成的符號(hào)是C規(guī)則下的符號(hào),
int main()
{
int ret=0;
ret=sum(1,2);
printf("%d\n",ret);
return 0;
}
test.cpp
extern "C"
{
int sum(int a, int b)//此時(shí)CPP文件函數(shù)在extern “C”里,CPP文件不再按CPP規(guī)則生成符號(hào),
{ //而是按照C規(guī)則生成符號(hào),這樣和main.c里的sum符號(hào)名相同,在鏈接的時(shí)候
return a+b; //能夠找到test.cpp里sum的定義。如果不加extern“C”,生成的符號(hào)為C++規(guī)則下 //的符號(hào),main.c里的sum和這里的sum符號(hào)不同,在連接時(shí), 就找不到符號(hào)的定義,就會(huì)報(bào)鏈接時(shí)的錯(cuò)誤
}
}
2、C++調(diào)用C
main.cpp
#include<iostream>
using namespace std;
extern "C" int sum(int a,int b);//告訴編譯器,按C規(guī)則生成sum符號(hào),這樣才會(huì)和test.c里的sum一樣
int main() //鏈接時(shí)才能找到符號(hào)的定義
{
int ret=0;
ret=sum(1,2);
cout<<ret<<endl;
return 0;
}
test.c
int sum(int a, int b)//C規(guī)則生成sum符號(hào)
{
return a+b;
}
由上我們可以看出extern “C”只有在C++文件才有的語法,而C文件不存在,所以處理的時(shí)候都是在C++的代碼里進(jìn)行處理。C++不僅能生成C++規(guī)則的符號(hào),還能生成C規(guī)則的符號(hào)。
… 但是,但是,一般情況下,一個(gè)公司購買別的公司的軟件產(chǎn)品,別人不會(huì)把自己辛辛苦苦寫的源代碼給你的,給你的只是個(gè)接口,一般情況下是一個(gè)庫,那這個(gè)時(shí)候我們就看不到源代碼,更用不了源代碼。此時(shí)又該如何實(shí)現(xiàn)C和C++的相互調(diào)用呢,由以上知道源碼的情況下,我們知道,C和C++相互調(diào)用時(shí),C源碼并沒有發(fā)生任何改變,改變的只是C++的代碼,所以,在沒有源碼的情況下,C++調(diào)用C和上面的情況一樣。接下來我們就詳細(xì)討論一下如何在只有一個(gè)C++庫的情況下,用C調(diào)用C++庫
假如sum.cpp如下,實(shí)際上用戶是看不見的。
intsum(int a,int b)
{
return a+b;
}
g++ -shared -fpic -o libsum.so sum.cpp 之后,我們得到一個(gè)libsum.so庫。我們知道這一個(gè)庫,它的源碼我們是不知道的,那此時(shí)我們?cè)撊绾斡肅調(diào)用它來實(shí)現(xiàn)自己的加法呢。此時(shí)我們就借助計(jì)算機(jī)世界里一句名言:計(jì)算機(jī)科學(xué)領(lǐng)域的任何問題都可以通過增加一個(gè)間接的中間層來解決。我們想辦法把libsum.so里的sum封裝起來。如下:
mysum.cpp
int sum(int a,int b);
extern "C"
{
int mysum(int a,int b)
{
return sum(a,b);//此時(shí)調(diào)用mysum()就相當(dāng)于調(diào)用sum();
}
}
那我在C里再次調(diào)用sum時(shí),我就調(diào)用封裝的mysum(),
#include<stdio.h>
int mysum(int a,int b);
int main()
{
int ret=0;
ret=mysum(10,20);
printf("%d\n",ret);
}
gcc -o run main.c mysum.cpp ./libsum.so生成run 大功告成。