由於項目須要,須要優化已有的Python代碼。目前Python代碼的執行過程是將Python代碼轉變成一行行指令,而後解釋器解釋指令的執行,調用到C代碼層。若是去掉指令解釋這個階段,直接進入C代碼層,效率就比較高了。若是用以前所述的使用Python C API將Python代碼改造爲C代碼並做爲Python的內建模塊,工做量極其大,也不能保證其正確性,因此這種方法不太現實。而Cython庫正好符合這種場景需求,將已有的Python代碼轉化爲C語言的代碼,並做爲Python的built-in模塊擴展。python
版本說明:linux
Python 2.7.13 (CPython)git
Cython 0.25.2github
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代碼同樣容易。
其最重要的功能是:
即 將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版本設置不一樣。
這裏採用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/