5分鐘掌握在 Cython 中使用 C++
1. 在Jupyter Notebook上使用C++
首先加載Cython擴(kuò)展,使用魔術(shù)命令 %load_ext Cython接下來運(yùn)行Cython代碼,使用魔術(shù)命令 %%cython --cplus如果使用MacOS,使用魔術(shù)命令 %%cython --cplus --compile-args=-stdlib=libc++ --link-args=-stdlib=libc++,詳情請參考https://stackoverflow.com/questions/57367764/cant-import-cpplist-into-cython
%load_ext?Cython
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
#?注意:?使用?'cimport'?而不是?'import'
from?libcpp.string?cimport?string
cdef?string?s
s?=?b"Hello?world!"
print(s.decode("utf-8"))
Hello?world!
2. C++和Python類型的相互轉(zhuǎn)換
3. 使用C++ STL
3.1 使用C++ Vector
可以替代Python的List。
初始化 - 通過Python的可迭代對象進(jìn)行初始化,需要聲明變量的嵌套類型
遍歷 - 讓index自增,通過while循環(huán)進(jìn)行遍歷
訪問 - 和Python一樣使用'[]'操作符對元素進(jìn)行訪問
追加 - 與Python list的append方法相似,使用C++ Vector的push_back方法追加元素
最后,我們通過分別實(shí)現(xiàn)Python和C++版本的元素計數(shù)函數(shù)來對比性能,C++大約快240倍左右。
注意: 為了公平起見,函數(shù)沒有傳入?yún)?shù),而是直接訪問函數(shù)體外部的變量。避免計入C++版本把Python列表轉(zhuǎn)換為C++ Vector的耗時。如果計入這部分耗時,C++的版本大約快4倍左右。
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
from?libcpp.vector?cimport?vector
#?通過Python對象初始化
cdef?vector[int]?vec?=?range(5)
#?遍歷
cdef:
????int?i?=?0
????int?n?=?vec.size()
print("開始遍歷...")
while?i?????#?訪問
????print("\t第%d個位置的元素是%d"?%?(i,?vec[i]))
????i?+=?1
print()
#?追加
vec.push_back(5)
print("追加元素之后vec變?yōu)?,?vec)
開始遍歷...
????????第0個位置的元素是0
????????第1個位置的元素是1
????????第2個位置的元素是2
????????第3個位置的元素是3
????????第4個位置的元素是4
????追加元素之后vec變?yōu)?[0,?1,?2,?3,?4,?5]
arr?=?[x?//?100?for?x?in?range(1000)]
target?=?6
def?count_py():
????return?sum(1?for?x?in?arr?if?x?==?target)
print("用Python來實(shí)現(xiàn),計算結(jié)果為%d!"%?count_py())
用Python來實(shí)現(xiàn),計算結(jié)果為100!
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
from?libcpp.vector?cimport?vector
cdef:
????int?target?=?6
????vector[int]?v?=?[x?//?100?for?x?in?range(1000)]
cdef?int?_count_cpp():
????cdef:
????????int?i?=?0
????????int?n?=?v.size()
????????int?ret?=?0
????while?i?????????if?v[i]?==?target:
????????????ret?+=?1
????????i?+=?1
????return?ret
def?count_cpp():
????return?_count_cpp()
print("用Cython(C++)來實(shí)現(xiàn),計算結(jié)果為%d!"%?count_cpp())
用Cython(C++)來實(shí)現(xiàn),計算結(jié)果為100!
print("對比Python版本與C++版本的性能...")
%timeit?count_py()
%timeit?count_cpp()
對比Python版本與C++版本的性能...??
30.8?μs?±?254?ns?per?loop?(mean?±?std.?dev.?of?7?runs,?10000?loops?each)??
130?ns?±?6.4?ns?per?loop?(mean?±?std.?dev.?of?7?runs,?10000000?loops?each)
3.2 使用C++ Unordered Map
可以替代Python的Dict。
初始化 - 通過Python的可迭代對象進(jìn)行初始化,需要聲明變量的嵌套類型
遍歷 - 讓泛型指針自增,通過while循環(huán)進(jìn)行遍歷
訪問 - 使用deref(C++中的'*'操作符)來解引用,返回pair對象,通過.first來訪問key, .second來訪問Value
查找 - 使用unordered_map.count,返回1或0;或者用unordered_map.find,返回一個泛型指針,如果指針指向unordered_map.end,則表示未找到。
追加/修改 - unordered_map[key] = value。如果Key不存在,'[]'操作符會添加一個Key,并賦值為默認(rèn)的Value,比如0.0。所以,除非確定不會產(chǎn)生錯誤,否則在修改Key對應(yīng)的Value之前,要先判斷Key是否存在。這與Python的DecaultDict有點(diǎn)相似。
最后,我們通過分別實(shí)現(xiàn)Python和C++版本的map條件求和函數(shù)來對比性能,C++大約快40倍左右。
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
from?cython.operator?cimport?dereference?as?deref,?preincrement?as?inc
from?libcpp.unordered_map?cimport?unordered_map
#?通過Python對象初始化
cdef?unordered_map[int,?float]?mymap?=?{i:?i/10?for?i?in?range(10)}
#?遍歷
cdef:
????unordered_map[int,?float].iterator?it?=?mymap.begin()
????unordered_map[int,?float].iterator?end?=?mymap.end()
print("開始遍歷...")
while?it?!=?end:
????#?訪問
????print("\tKey?is?%d,?Value?is?%.1f"?%?(deref(it).first,?deref(it).second))
????inc(it)
print()
#?查找
print("開始查找...")
if?mymap.count(-2):
????print("\t元素-2存在!")
else:
????print("\t元素-2不存在!")
it?=?mymap.find(3)
if?it?!=?end:
????print("\t元素3存在,?它的值是%.1f!"?%?deref(it).second)
else:
????print("\t元素3不存在!")
print()
#?修改
print("修改元素...")
if?mymap.count(3):
????mymap[3]?+=?1.0
mymap[-2]??#?Key?-2不存在,會被添加一個默認(rèn)值0.0
print("\tKey?is?3,?Value?is?%.1f"?%?mymap[3])
print("\tKey?is?-2,?Value?is?%.1f"?%?mymap[-2])
開始遍歷...
????????Key?is?0,?Value?is?0.0
????????Key?is?1,?Value?is?0.1
????????Key?is?2,?Value?is?0.2
????????Key?is?3,?Value?is?0.3
????????Key?is?4,?Value?is?0.4
????????Key?is?5,?Value?is?0.5
????????Key?is?6,?Value?is?0.6
????????Key?is?7,?Value?is?0.7
????????Key?is?8,?Value?is?0.8
????????Key?is?9,?Value?is?0.9
開始查找...
????????元素-2不存在!
????????元素3存在,?它的值是0.3!
修改元素...
????????Key?is?3,?Value?is?1.3
????????Key?is?-2,?Value?is?0.0
my_map?=?{x:?x?for?x?in?range(100)}
target?=?50
def?sum_lt_py():
????return?sum(my_map[x]?for?x?in?my_map?if?x?
print("用Python來實(shí)現(xiàn),計算結(jié)果為%d!"%?sum_lt_py())
用Python來實(shí)現(xiàn),計算結(jié)果為1225!
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
from?libcpp.unordered_map?cimport?unordered_map
from?cython.operator?cimport?dereference?as?deref,?preincrement?as?inc
cdef:
????unordered_map[int,?int]?my_map?=?{x:?x?for?x?in?range(100)}
????int?target?=?50
cdef?_sum_lt_cpp():
????cdef:
????????unordered_map[int,?int].iterator?it?=?my_map.begin()
????????int?ret
????while?it?!=?my_map.end():
????????if?deref(it).first?????????????ret?+=?deref(it).second
????????inc(it)
????return?ret
def?sum_lt_cpp():
????return?_sum_lt_cpp()
print("用Cython(C++)來實(shí)現(xiàn),計算結(jié)果為%d!"%?sum_lt_cpp())
用Cython(C++)來實(shí)現(xiàn),計算結(jié)果為1225!
print("對比Python版本與C++版本的性能...")
%timeit?sum_lt_py()
%timeit?sum_lt_cpp()
對比Python版本與C++版本的性能...
????6.63?μs?±?183?ns?per?loop?(mean?±?std.?dev.?of?7?runs,?100000?loops?each)
????157?ns?±?3.13?ns?per?loop?(mean?±?std.?dev.?of?7?runs,?10000000?loops?each)
3.3 使用C++ Unordered Set
可以替代Python的Set。
初始化 - 通過Python的可迭代對象進(jìn)行初始化,需要聲明變量的嵌套類型
遍歷 - 讓泛型指針自增,通過while循環(huán)進(jìn)行遍歷
訪問 - 使用deref(C++中的'*'操作符)來解引用
查找 - 使用unordered_set.count,返回1或0
追加 - 使用unordered_set.insert,如果元素已經(jīng)存在,則元素不會被追加
交集、并集、差集 - 據(jù)我所知,unordered_set的這些操作需要開發(fā)者自己去實(shí)現(xiàn),不如Python的Set用起來方便。
最后,我們通過分別實(shí)現(xiàn)Python和C++版本的set求交集對比性能,C++大約慢20倍左右。詳情可參考https://stackoverflow.com/questions/54763112/how-to-improve-stdset-intersection-performance-in-c如果只是求兩個集合相同元素的數(shù)量,C++的性能大約是Python的6倍。不難推測,C++的unordered set查詢很快,但是創(chuàng)建很慢。
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
from?cython.operator?cimport?dereference?as?deref,?preincrement?as?inc
from?libcpp.unordered_set?cimport?unordered_set
#?通過Python對象初始化
cdef?unordered_set[int]?myset?=?{i?for?i?in?range(5)}
#?遍歷
cdef:
????unordered_set[int].iterator?it?=?myset.begin()
????unordered_set[int].iterator?end?=?myset.end()
print("開始遍歷...")
while?it?!=?end:
????#?訪問
????print("\tValue?is?%d"?%?deref(it))
????inc(it)
print()
#?查找
print("開始查找...")
if?myset.count(-2):
????print("\t元素-2存在!")
else:
????print("\t元素-2不存在!")
print()
#?追加
print("追加元素...")
myset.insert(0)
myset.insert(-1)
print("\tMyset?is:?",?myset)
開始遍歷...
????????Value?is?0
????????Value?is?1
????????Value?is?2
????????Value?is?3
????????Value?is?4
開始查找...
????????元素-2不存在!
追加元素...
????????Myset?is:??{0,?1,?2,?3,?4,?-1}
myset1?=?{x?for?x?in?range(100)}
myset2?=?{x?for?x?in?range(50,?60)}
def?intersection_py():
????return?myset1?&?myset2
print("用Python來實(shí)現(xiàn),計算結(jié)果為%s!"%?intersection_py())
用Python來實(shí)現(xiàn),計算結(jié)果為{50,?51,?52,?53,?54,?55,?56,?57,?58,?59}!
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
from?cython.operator?cimport?dereference?as?deref,?preincrement?as?inc
from?libcpp.unordered_set?cimport?unordered_set
cdef:
????unordered_set[int]?myset1?=?{x?for?x?in?range(100)}
????unordered_set[int]?myset2?=?{x?for?x?in?range(50,?60)}
cdef?unordered_set[int]?_intersection_cpp():
????cdef:
????????unordered_set[int].iterator?it?=?myset1.begin()
????????unordered_set[int]?ret
????while?it?!=?myset1.end():
????????if?myset2.count(deref(it)):
????????????ret.insert(deref(it))
????????inc(it)
????return?ret
def?intersection_cpp():
????return?_intersection_cpp()
print("用Cython(C++)來實(shí)現(xiàn),計算結(jié)果為%s!"%?intersection_cpp())
用Cython(C++)來實(shí)現(xiàn),計算結(jié)果為{50,?51,?52,?53,?54,?55,?56,?57,?58,?59}!
print("對比Python版本與C++版本的性能...")
%timeit?intersection_py()
%timeit?intersection_cpp()
對比Python版本與C++版本的性能...
????244?ns?±?2.96?ns?per?loop?(mean?±?std.?dev.?of?7?runs,?1000000?loops?each)
????4.87?μs?±?100?ns?per?loop?(mean?±?std.?dev.?of?7?runs,?100000?loops?each)
myset1?=?{x?for?x?in?range(100)}
myset2?=?{x?for?x?in?range(50,?60)}
def?count_common_py():
????return?len(myset1?&?myset2)
print("用Python(C++)來實(shí)現(xiàn),計算結(jié)果為%s!"%?count_common_py())
用Python(C++)來實(shí)現(xiàn),計算結(jié)果為10!
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
from?cython.operator?cimport?dereference?as?deref,?preincrement?as?inc
from?libcpp.unordered_set?cimport?unordered_set
cdef:
????unordered_set[int]?myset2?=?{x?for?x?in?range(100)}
????unordered_set[int]?myset1?=?{x?for?x?in?range(50,?60)}
cdef?int?_count_common_cpp():
????if?myset1.size()?>?myset2.size():
????????myset1.swap(myset2)
????cdef:
????????unordered_set[int].iterator?it?=?myset1.begin()
????????int?ret?=?0
????while?it?!=?myset1.end():
????????if?myset2.count(deref(it)):
????????????ret?+=?1
????????inc(it)
????return?ret
def?count_common_cpp():
????return?_count_common_cpp()
print("用Cython(C++)來實(shí)現(xiàn),計算結(jié)果為%s!"%?count_common_cpp())
用Cython(C++)來實(shí)現(xiàn),計算結(jié)果為10!
print("對比Python版本與C++版本的性能...")
%timeit?count_common_py()
%timeit?count_common_cpp()
對比Python版本與C++版本的性能...
276?ns?±?3.18?ns?per?loop?(mean?±?std.?dev.?of?7?runs,?1000000?loops?each)
46.2?ns?±?0.845?ns?per?loop?(mean?±?std.?dev.?of?7?runs,?10000000?loops?each)
4. 傳值與傳引用
Python的函數(shù),如果是容器類對象(如List, Set),傳遞的是引用,否則傳遞的是值(如int, float),如果不希望讓函數(shù)修改容器類對象,可以用deepcopy函數(shù)先拷貝一份容器的副本。但在C++里默認(rèn)都是傳值,如果需要傳引用需要聲明。
以int型Vector為例,可以看到v1的值沒有被pass_value修改,但被pass_reference修改了。
傳值使用
vector[int],pass_value函數(shù)只是傳入了v1的一份拷貝,所以函數(shù)無法修改v1傳引用使用
vector[int]&,pass_reference傳入了v1的引用,函數(shù)可以修改v1。
下面的兩塊代碼可以展示Python與C++的不同之處。
from?copy?import?deepcopy
def?pass_value(v):
????v?=?deepcopy(v)
????v[0]?=?-1
def?pass_reference(v):
????v[0]?=?-1
v1?=?[0,?0,?0]
print("v1的初始值是%s"?%?v1)
pass_value(v1)
print("執(zhí)行pass_value函數(shù)后,v1的值是%s"?%?v1)
pass_reference(v1)
print("執(zhí)行pass_reference函數(shù)后,v1的值是%s"?%?v1)
v1的初始值是[0,?0,?0]
執(zhí)行pass_value函數(shù)后,v1的值是[0,?0,?0]
執(zhí)行pass_reference函數(shù)后,v1的值是[-1,?0,?0]
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
from?libcpp.vector?cimport?vector
cdef?void?pass_value(vector[int]?v):
????v[0]?=?-1
cdef?void?pass_reference(vector[int]&?v):
????v[0]?=?-1
cdef?vector[int]?v1?=?[0,?0,?0]
print("v1的初始值是%s"?%?v1)
pass_value(v1)
print("執(zhí)行pass_value函數(shù)后,v1的值是%s"?%?v1)
pass_reference(v1)
print("執(zhí)行pass_reference函數(shù)后,v1的值是%s"?%?v1)
v1的初始值是[0,?0,?0]
執(zhí)行pass_value函數(shù)后,v1的值是[0,?0,?0]
執(zhí)行pass_reference函數(shù)后,v1的值是[-1,?0,?0]
5. 數(shù)字的范圍
Python只有int型,而且int的范圍可以認(rèn)為是無限大的,只要沒有超出內(nèi)存限制,所以Python使用者一般不太關(guān)心數(shù)值溢出等問題。但使用C++的時候就需要謹(jǐn)慎,C++各個數(shù)字類型對應(yīng)的范圍如下:


比如下面的函數(shù)就會造成錯誤。
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
def?sum_py(num1,?num2):
????print("The?result?by?python?is:",?num1?+?num2)
cdef?int?_sum_cpp(int?num1,?int?num2):??#?int?->?long?int
????return?num1?+?num2
def?sum_cpp(num1,?num2):
????print("The?result?by?cpp?is:",?_sum_cpp(num1,?num2))
sum_py(2**31-1,?1)
sum_cpp(2**31-1,?1)
The?result?by?python?is:?2147483648
The?result?by?cpp?is:?-2147483648
%%cython?--cplus?--compile-args=-stdlib=libc++?--link-args=-stdlib=libc++
from?libcpp?cimport?bool
def?lt_py(num1,?num2):
????print("The?result?by?python?is:",?num1?
cdef?bool?_lt_cpp(float?num1,?float?num2):??#?float?->?double
????return?num1?>?num2
def?lt_cpp(num1,?num2):
????print("The?result?by?cpp?is:",?_lt_cpp(num1,?num2))
lt_py(1234567890.0,?1234567891.0)
lt_cpp(1234567890.0,?1234567891.0)
The?result?by?python?is:?True
The?result?by?cpp?is:?False
作者:李小文,先后從事過數(shù)據(jù)分析、數(shù)據(jù)挖掘工作,主要開發(fā)語言是Python,現(xiàn)任一家小型互聯(lián)網(wǎng)公司的算法工程師。
Github:?https://github.com/tushushu
推薦閱讀


點(diǎn)擊下方閱讀原文加入社區(qū)會員
點(diǎn)贊鼓勵一下

