1. 程式人生 > >Cython的用法以及填坑姿勢

Cython的用法以及填坑姿勢

visual 總結 字節 als 文本編輯器 ref 進入 類型 code

因為項目需要,需要優化已有的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的用法以及填坑姿勢