cython python3_30倍!使用Cython加速Python代码
原標題:30倍!使用Cython加速Python代碼
作者:George Seif、Thomas Wolf、Lukas Frei
編譯:1+1=6 | 公眾號海外部
前言
你可能經常會一次又一次地聽到關于Python的抱怨,Python跑起來太慢了!
與許多其他編程語言相比,Python的確很慢。
有幾種不同的方法可以使代碼提速:
如果你的代碼是純Python。如果你有一個很大的for循環,你只能使用它,而不能放入矩陣中,因為數據必須按順序處理,那該怎么辦?有沒有辦法加快Python本身的速度?
來吧,看看Cython!
文末下載Cython相關書籍
什么是Cython?
Cython的核心是Python和C / C++之間的一個中間步驟。它允許N你編寫純Python代碼,只需要做一些小修改,然后將其直接翻譯成C代碼。
Cython 語言是 Python 的一個超集,它包含有兩種類型的對象:
Python 對象就是我們在常規 Python 中使用到的那些對象,諸如數值、字符串、列表和類實例等等。
Cython C 對象就是那些 C 和 C++ 對象,諸如雙精度、整型、浮點數、結構和向量,它們能夠由 Cython 在超級高效的低級語言代碼中進行編譯。
你對Python代碼所做的唯一調整就是向每個變量添加類型信息。通常,我們可以像這樣在Python中聲明一個變量:
x = 0.5
使用Cython,我們為該變量添加一個類型:
cdef float x = 0.5
這告訴Cython,變量是浮點數,就像我們在C中所做的一樣。對于純Python,變量的類型是動態確定的。Cython中類型的顯式聲明使其轉為C代碼成為可能,因為顯式類型聲明需要+。
有很多辦法來測試、編譯和發布 Cython 代碼。Cython 甚至可以像 Python 一樣直接用于 Jupyter Notebook 中。有很多辦法來測試、編譯和發布 Cython 代碼。Cython 甚至可以像 Python 一樣直接用于 Jupyter Notebook 中。
安裝Cython只需要一行pip:
pip install cython
使用Cython需要安裝C語言編譯器,因此,安裝過程會根據你當前的操作系統而有所不同。對于Linux,通常使用GNU C編譯器(gncc)。對于Mac OS,你可以下載Xcode以獲取gncc。而Windows 桌面系統下安裝C編譯器會更復雜。
使用 %load_ext Cython 指令在 Jupyter notebook 中加載 Cython 擴展。
然后通過指令 %%cython,我們就可以像 Python 一樣在 Jupyter notebook 中使用 Cython。
如果在執行 Cython 代碼的時候遇到了編譯錯誤,請檢查 Jupyter 終端的完整輸出信息。
大多數情況下可能都是因為在 %%cython 之后遺漏了 -+ 標簽(比如當你使用 spaCy Cython 接口時)。如果編譯器報出了關于 Numpy 的錯誤,那就是遺漏了 import numpy。
如果你要在在IPython中使用Cython:
首先介紹一下IPython Magic命令。Magic命令以百分號開頭,通常有2種類型:
單行Magic由單個'%'表示,并且僅在一行輸入上操作。
單元格Magic用兩個'%'表示,并在多行輸入上操作。
首先運行下列語句引入Cython:
%load_ext Cython
然后,當運行Cython代碼時,我們需要加入以下Cython 代碼:
%%cython
然后就可以愉快地使用Cython了。
Cython中的類型
使用Cython時,變量和函數有兩組不同的類型。
對于變量,我們有:
cdef int a, b, c
cdef char *s
cdef float x = 0.5 (single precision)
cdef double x = 63.4 (double precision)
cdef list names
cdef dict goals_for_each_play
cdef object card_deck
注意所有這些類型都來自C / C++ !
def - 常規Python函數,僅從Python調用。
cdef - 僅限Cython函數,接受Python對象或C值作為參數,并且可以返回Python對象或C值,cdef函數不能直接在Python中調用。
cpdef - 接受Python對象或C值作為參數,并且可以返回Python對象或C值。
我們可以方便的向C代碼傳遞和返回結果,Cython會自動為我們做相應的類型轉化。
了解了Cython類型之后,我們就可以直接實現加速了!
如何使用Cython加速代碼
我們要做的第一件事是設置Python代碼基準:用于計算數字階乘的for循環。原始Python代碼如下:
deftest(x):
y = 1
fori inrange(x+1):
y *= i
returny
Cython的實現過程看起來非常相似。首先,確保Cython代碼文件具有 .pyx 擴展名。這些文件將被 Cython 編譯器編譯成 C 或 C++ 文件,再進一步地被 C 編譯器編譯成字節碼文件。
你也可以使用 pyximport 將一個 .pyx 文件直接加載到 Python 程序中:
importpyximport; pyximport.install
importmy_cython_module
你也可以將自己的 Cython 代碼作為 Python 包構建,然后像正常的 Python 包一樣將其導入或者發布。不過這種做法需要花費更多的時間,特別是你需要讓 Cython 包能夠在所有的平臺上運行。如果你需要一個參考樣例,不妨看看 spaCy 的安裝腳本:
https://github.com/explosion/spaCy/blob/master/setup.py?source=post_page---------------------------
最終 Python 解釋器將能夠調用這些字節碼文件。對代碼本身的惟一更改是,我們已經聲明了每個變量和函數的類型。
cpdef int test(int x):
cdef int y = 1
cdef int i
fori inrange(x+1):
y *= i
returny
注意函數有一個cpdef來確保我們可以從Python調用它。另外看看我們的循環變量i是如何具有類型的。你需要為函數中的所有變量設置類型,以便C編譯器知道使用哪種類型!
接下來,創建一個setup.py文件,該文件將Cython代碼編譯為C代碼:
fromdistutils.core importsetup
fromCython.Build importcythonize
setup(ext_modules = cythonize('run_cython.pyx'))
并執行編譯:
python setup.py build_ext --inplace
Boom!我們的C代碼已經編譯好,可以使用了!
你將看到,在Cython代碼所在的文件夾中,擁有運行C代碼所需的所有文件,包括run_cython.c文件。如果你感興趣,可以查看一下Cython生成的C代碼!
現在我們準備測試新的C代碼!查看下面的代碼,它將執行一個速度測試,將原始Python代碼與Cython代碼進行比較。
現在我們準備測試我們新的超快速C代碼了!查看下面的代碼,它執行速度測試以將原始Python代碼與Cython代碼進行比較。
importrun_python
importrun_cython
importtime
number = 10
start = time.time
run_python.test(number)
end = time.time
py_time = end - start
print("Python time = {}".format(py_time))
start = time.time
run_cython.test(number)
end = time.time
cy_time = end - start
print("Cython time = {}".format(cy_time))
print("Speedup = {}".format(py_time / cy_time))
Cython可以讓你在幾乎所有原始Python代碼上獲得良好的加速,而不需要太多額外的工作。需要注意的關鍵是,循環次數越多,處理的數據越多,Cython可以提供的幫助就越多。
查看下表,該表顯示了Cython為不同的階乘值提供的速度我們使用Cython獲得了超過36倍的加速!
Cython在NLP中的加速應用
當我們在操作字符串時,要如何在 Cython 中設計一個更加高效的循環呢?spaCy是個不錯的選擇!
spaCy 中所有的unicode字符串(the text of a token, its lower case text, its lemma form, POS tag label, parse tree dependency label, Named-Entity tags…)都被存儲在一個稱為StringStore的數據結構中,它通過一個64位哈希碼進行索引,例如C類型的 uint64_t。
StringStore對象實現了Python unicode字符串與 64 位哈希碼之前的查找映射。
它可以spaCy的任何地方和任意對象進行訪問,例如npl.vocab.strings、doc.vocab.strings或者span.doc.vocab.string。
當某模塊需要在某些標記上獲得更快的處理速度時,可以使用C語言類型的64位哈希碼代替字符串來實現。調用StringStore查找表將返回與該哈希碼相關聯的Python unicode字符串。
但是spaCy能做的可不僅僅只有這些,它還允許我們訪問文檔和詞匯表完全填充的C語言類型結構,我們可以在Cython循環中使用這些結構,而不必去構建自己的結構。
spaCy拓展:
https://spacy.io/api/cython?source=post_page---------------------------
建立一個腳本用于創建一個包含有 10 份文檔的列表,每份文檔都大概含有 17 萬個單詞,采用 spaCy 進行分析。當然我們也可以對 17 萬份文檔(每份文檔包含 10 個單詞)進行分析,但是這樣做會導致創建的過程非常慢,所以我們還是選擇了 10 份文檔。
我們想要在這個數據集上展開某些自然語言處理任務。例如,我們可以統計數據集中單詞「run」作為名詞出現的次數(例如,被 spaCy 標記為「NN」詞性標簽)。
采用Python循環來實現上述分析過程非常簡單和直觀:
importurllib.request
importspacy
withurllib.request.urlopen('https://raw.githubusercontent.com/pytorch/examples/master/word_language_model/data/wikitext-2/valid.txt') asresponse:
text = response.read
nlp = spacy.load('en')
doc_list = list(nlp(text[:800000].decode('utf8')) fori inrange(10))
這段代碼至少需要運行 1.4 秒才能獲得答案。如果我們的數據集中包含有數以百萬計的文檔,為了獲得答案,我們也許需要花費超過一天的時間。
我們也許能夠采用多線程來實現加速,但是在Python中這種做法并不是那么明智,因為你還需要處理全局解釋器鎖(GIL)。在Cython中可以無視GIL的存在而盡情使用線程加速。但不能再使用Python中的字典和列表,因為Python中的變量都自動帶了鎖(GIL)。還好Cython已經封裝了C++標準庫中的容器:deque,list,map,pair,queue,set,stack,vector。完全可以替代Python的dict, list, set等。
我們使用Cython就可以解決這個,但不能再使用Python中的字典和列表,因為Python中的變量都自動帶了鎖(GIL)。還好Cython已經封裝了C++標準庫中的容器:deque,list,map,pair,queue,set,stack,vector。完全可以替代Python的dict, list, set等。
另外請注意,Cython也可以使用多線程!Cython在后臺可以直接調用OpenMP。
https://cython.readthedocs.io/en/latest/src/userguide/parallelism.html?source=post_page---------------------------
現在讓我們嘗試使用spaCy和Cython來加速 Python 代碼。
首先需要考慮好數據結構,我們需要一個C類型的數組來存儲數據,需要指針來指向每個文檔的 TokenC 數組。我們還需要將測試字符(「run」和「NN」)轉成 64 位哈希碼。
當所有需要處理的數據都變成了C類型對象,我們就可以以純C語言的速度對數據集進行迭代。
以下是被轉換成Cython和spaCy的實現:
%%cython -+
importnumpy
fromcymem.cymem cimport Pool
fromspacy.tokens.doc cimport Doc
fromspacy.typedefs cimport hash_t
fromspacy.structs cimport TokenC
cdef struct DocElement:
TokenC* c
int length
cdef int fast_loop(DocElement* docs, int n_docs, hash_t word, hash_t tag):
cdef int n_out = 0
fordoc indocs[:n_docs]:
forc indoc.c[:doc.length]:
ifc.lex.lower == word andc.tag == tag:
n_out += 1
returnn_out
defmain_nlp_fast(doc_list):
cdef int i, n_out, n_docs = len(doc_list)
cdef Pool mem = Pool
cdef DocElement* docs = mem.alloc(n_docs, sizeof(DocElement))
cdef Doc doc
fori, doc inenumerate(doc_list):
docs[i].c = doc.c
docs[i].length = (doc).length
word_hash = doc.vocab.strings.add('run')
tag_hash = doc.vocab.strings.add('NN')
n_out = fast_loop(docs, n_docs, word_hash, tag_hash)
在Jupyter notebook上,這段Cython代碼運行了大概20毫秒,比之前的純Python循環快了大概80倍。
使用Jupyter notebook單元編寫模塊的速度很可觀,它可以與其它 Python 模塊和函數自然地連接:在 20 毫秒內掃描大約 170 萬個單詞,這意味著我們每秒能夠處理高達 8 千萬個單詞。
如果你已經了解C語言,Cython還允許訪問C代碼,而Cython的創建者還沒有為這些代碼添加現成的聲明。例如,使用以下代碼,可以為C函數生成Python包裝器并將其添加到模塊dict中。
%%cython
cdef extern from"math.h":
cpdef double sin(double x)
Cython注意的坑
1、.pyx中用CDEF定義的東西,除類以外對的.py都是不可見的。
2、.c中是不能操作C類型的,如果想在.py中操作C類型就要在.pyx中從python對象轉成C類型或者用含有set / get方法的C類型包裹類。
3、雖然Cython能對Python的str和C的“char *”之間進行自動類型轉換,但是對于“char a [n]”這種固定長度的字符串是無法自動轉換的。需要使用Cython的libc.string .strcpy進行顯式拷貝。
4、回調函數需要用函數包裹,再通過C的“void *”強制轉換后才能傳入C函數。
Cython相關資料(下載)
0、其他:
https://cython.org/?source=post_page---------------------------
1、官方文檔:
2、參考書籍(文末下載):
書籍下載
在后臺輸入(嚴格大小寫)
責任編輯:
總結
以上是生活随笔為你收集整理的cython python3_30倍!使用Cython加速Python代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux中移动光标cw什么意思,Lin
- 下一篇: python历史波动率_历史波动率计算(