Cython的用法以及填坑姿勢
因為項目需要,需要優化已有的Python代碼。目前Python代碼的執行過程是將Python代碼轉變成一行行指令,然後解釋器解釋指令的執行,調用到C代碼層。如果去掉指令解釋這個階段,直接進入C代碼層,效率就比較高了。如果用之前所述的使用Python C API將Python代碼改造為C代碼並作為Python的內建模塊,工作量極其大,也不能保證其正確性,所以這種方法不太現實。而Cython庫正好符合這種場景需求,將已有的Python代碼轉化為C語言的代碼,並作為Python的built-in模塊擴展。
版本說明:
Python 2.7.13 (CPython)
Cython 0.25.2
Python的文件類型介紹:
.py python的源代碼文件
.pyc Python源代碼import後,編譯生成的字節碼
.pyo Python源代碼編譯優化生成的字節碼。pyo比pyc並沒有優化多少,只是去掉了斷言
.pyd Python的動態鏈接庫(Windows平臺)
.py, .pyc, .pyo 運行速度幾乎無差別,只是pyc, pyo文件加載的速度更快,不能用文本編輯器查看內容,反編譯不太容易
本文的目標是將test.py文件生成test.c文件,然後將test.c文件作為Python源碼的一部分,重新編譯生成Python,使用時直接import test即可使用test模塊。
Cython基本介紹:
文檔中這樣總結Cython:
Cython is an optimising static compiler for both the Python programming language and the extended Cython programming language (based on Pyrex). It makes writing C extensions for Python as easy as Python itself.
是一個Python編程語言的編譯器,寫C擴展就像寫Python代碼一樣容易。
其最重要的功能是:
- write Python code that calls back and forth from and to C or C++ code natively at any point.
即 將Python代碼翻譯為C代碼。之後就可以像前面文章介紹的C語言擴展Python模塊使用這些C代碼了。
Cython基本用法:
在使用Cython編譯Python代碼時,務必要安裝C/C++編譯器,本文是直接安裝了Visiual Studio 2015的開發環境。
1. 安裝Cython庫
pip install Cython
就是如此簡單明了
2. 編寫一個測試代碼文件test.py放在D:/test/test.py
def say_hello(): print "hello world"
然後在同一目錄下,新建一個setup.py文件,內容如下:
from distutils.core import setup from Cython.Build import cythonize setup(ext_modules = cythonize("test.py"))
cythonize()是Cython提供將Python代碼轉換成C代碼的API,
setup是Python提供的一種發布Python模塊的方法。
3. 使用命令行編譯Python代碼:
python setup.py build_ext --inplace
如果出現這種情況是因為沒有C編譯器相關的配置沒有設置好,在Windows上一般采用Microsoft VisualStudio,不同的VS版本設置不同。
- Visual Studio 2010 (VS10): SET VS90COMNTOOLS=%VS100COMNTOOLS%
- Visual Studio 2012 (VS11): SET VS90COMNTOOLS=%VS110COMNTOOLS%
- Visual Studio 2013 (VS12): SET VS90COMNTOOLS=%VS120COMNTOOLS%
- Visual Studio 2015 (VS14): SET VS90COMNTOOLS=%VS140COMNTOOLS%
- Visual Studio 2017 (VS14): SET VS90COMNTOOLS=%VS150COMNTOOLS%
這裏采用VS2015作為C的編譯器。
在命令行中輸入SET VS90COMNTOOLS=%VS140COMNTOOLS%
然後輸入編譯命令:python setup.py build_ext --inplace
最終的生成結果如下:
在D:/test/ 目錄中:
test.c是test.py轉化後的C代碼文件,可以看到test.c非常大!!
test.pyd是python的動態鏈接庫,我們在使用import test時會加載
build目錄編譯過程中生成的臨時文件
使用剛剛生成的test模塊,就像使用Python的任意模塊一樣:
這裏稍微解釋一下 命令行:python setup.py build_ext --inplace
build_ext是指明python生成C/C++的擴展模塊(build C/C++ extensions (compile/link to build directory))
--inplace指示 將編譯後的擴展模塊直接放在與test.py同級的目錄中。
整個Cython工作的流程如下圖所示:
分兩步:
1).py文件使用Cython被編譯為.c文件;
2).c文件使用C編譯器生成.pyd(windos)或.so(linux)文件。
除了這種普遍的用法外,還可以在Python代碼的某些地方加上靜態類型聲明,也可以更進一步提升Python的運行效率,這些屬於小技巧了~
比如:
def say_hello(int s): cdef int a = 2 print s + 2
s和a變量直接指示為int類型,不用再做動態語言的類型推斷了。
小測試:
import math import time def f(): time1 = time.time() for i in range(100000000): x = math.sqrt(i) time2 = time.time() print time2 - time1
這段原生的Python代碼運行時間是13.17秒,使用Cython優化後,運行時間為9.36秒。基本上提升30%。其實Cython一般對外聲稱的效率提升也大概是這麽多。
Cython中的坑
在這一小節中,討論Cython中的一些坑以及填坑姿勢。Cython官方文檔中已經明確指出一些不支持的Python特性,有些不打算修復,再結合具體項目場景,給出一些坑的解決方案。
具體項目需求: 將一些需要優化的Python代碼模塊翻譯成C代碼,加入項目中,編譯鏈接之後,作為Python的一個built-in模塊。
所以,只需要轉換成C代碼這一步驟即可,不需要使用Python提供的distutils模塊,只需要Cython提供的cythonize。
1. 從Python的site-package中提取install的Cython目錄,獨立出來。因為是供給其他人使用,其他人pip install cython的話可能版本不一致,會出現一些問題。
Cython目錄是Cython源碼以及Python2.7/Lib/site-package下的cython.py,即:
CythonTool是封裝了轉化為C代碼的py腳本文件。
在使用時,需要設置一下sys.path,在import時才能找到我們獨立出來的Cython模塊。
# import Cython path
sys.path.insert(0, cython_path) from Cython.Build import cythonize from Cython.Compiler import Options
在sys.path的頭部添加cython_path,所以Python site-package裏的Cython就不會影響我們獨立出來的Cython模塊。
2. 在編譯python代碼為C代碼時,需要指定輸出的C代碼文件路徑,Cython默認的是python腳本目錄,這樣會導致py文件與.c文件混在一起,很容易就亂了。
目前工作目錄有三個
LibDir: 需要優化的Python腳本所在目錄
CfileDir: 輸出的C代碼文件所在的目錄
ToolDir: 封裝的cython優化腳本所在的目錄,其作用是將LibDir中的Python模塊轉換為C代碼,然後輸出到CfileDir
故而封裝的cython腳本工作目錄在ToolDir,腳本中最核心的是代碼是:
cythonize(pyfilePath, build_dir=CfileDir)
使用build_dir參數指明C代碼輸出目錄。
看起來很完美,但是Cython源碼在這裏裏有個坑。
當指定build_dir時,當pyfilePath與CfileDir都為絕對路徑時,且cython腳本的工作目錄與pyfilePath不一致時,cythonize會將輸出文件的目錄置為pyfilePath所在的目錄,故最後輸出的C代碼文件不會到CfileDir裏。
所以應該在封裝的cython腳本裏調用os.chdir(LibDir),轉換完成時再切換到原有工作目錄。牢記cython的工作目錄應該與待優化的python腳本目錄一致。
原因:cythonize中的實現有這樣一段代碼:【調試狀態下】
紅色框中,如果c_file是一個絕對文件名時,會出現以下情況,至於c_file為什麽會是一個絕對的文件名,是因為cython的工作目錄與待優化腳本目錄不匹配導致的。
3. 原始的Cython對Python的Package支持度不夠,一個大坑!!
只能通過修改Cython的源碼來填坑。
原始的Cython編譯Python之後,生成的C代碼裏有兩個關鍵的地方,拿test模塊為例:
這裏定義了test模塊初始化函數,這個函數裏會有創建test模塊的代碼部分:
當import時,Python解釋器會調用這裏,初始化test模塊,將test名字加入到sys.builtin_module_names中。
測試發現,如果有D:/Lib/mypackate/test.py , 編譯後,生成的C代碼與D:/Lib/test.py生成的代碼並無不同,即mypackate這個包被忽略了,導致生成的C代碼沒有了包依賴關系。
順著代碼閱讀,最終確定了問題出現的源頭,Cython/Compiler/ModuleNode.py, 修改了此文件中的兩個函數:
1)生成模塊init代碼函數:full_module_name替換掉env.module_name, 即用initmypackage_test替換init_test
2) 修改了創建模塊時傳入的模塊名規則,並考慮到mypackage/__init__.py這種情況, 對於package來說需要加入__path__用以標識這個對象不是普通的Python模塊,而是一個包。
4. 深坑。 inspect、types相關。
Inspect模塊中有各種類型判斷函數,比如 isfunction, ismethod, ismodule等。這裏的坑是:
cython化的函數類型變為了cython_function_or_method,而原始python的函數類型是function,所以如果待優化的Python腳本中使用isfunction(func, types.FunctionType)時,如果func是原始的函數則返回True,而cython化的函數返回False. 除了function類型外還有generator, functionType.func_globals類型也存在不一致。
目前在inspect.py的isfunction中加入了trick,會判斷
type(func).__name__=="cython_function_or_method". 並且types.py模塊不被cython化,那麽如果調用inspect.isfunction(func, types.FunctionType)對於原始的Python函數還是cython化的函數都沒有問題了。
但是如果直接使用isinstance(func, types.FunctionType)仍然會存在問題,types.FunctionType只對原始的python函數判斷正確。
比較繞,總而言之一句話,python裏的類型和cython化後的對應的類型可能會不同。我總結了大部分python類型,其中有幾個cython化後類型不一致:
沒有什麽太好的解決辦法,要麽改寫inspect模塊,但還要保證Python代碼不能直接使用types模塊,要麽修改Python源碼中關於isinstance的實現。
5. 官方文檔中列出的坑
1) 不支持Nested tuple, Python2中的特性,Python3不支持了。所以Cython直接不支持Nested tuple特性
2)找不到變量名:You can disable the latter behaviour by setting "error_on_unknown_names" to
解決辦法:
3)Stack Frames.
Cython不支持Stack Frame。
總結:可以考慮使用Cython優化一些簡單的Python項目,如果用到非常復雜的場景的話,有些語法的特性不支持,會有繞不過去的坑
參考資料:
https://github.com/cython/cython
https://mdqinc.com/blog/2011/08/statically-linking-python-with-cython-generated-modules-and-packages/
Cython的用法以及填坑姿勢