python如何編譯py文件生成pyc、pyo、pyd以及如何和C語言結合使用
喜歡這篇文章的話,就去bilibili看看我吧,雖然啥也沒有。:https://space.bilibili.com/12921175
python執行py文件的流程
當我們執行一個py文件的時候,直接python xx.py即可,那么這個流程是怎么樣的呢。先說明一下,python執行代碼實際上是先打開文件然后執行里面的代碼,所以文件的擴展名不一定是py的形式,txt形式也是依舊可以成功執行,只要文件里面的代碼是符合python規范的。下面我們來看看python是怎么執行py文件的。
先將文件里面的內容讀取出來,scanner對其進行掃描,切分成一個個的token
parser對token進行解析,建立抽象語法樹(AST,abstract syntax tree)
compiler對ast進行編譯,得到python字節碼
code evaluator執行字節碼
我們注意到第三個過程,是一個編譯的過程。說明python即便是解釋性語言,也依舊存在着編譯的過程,這一點和java是一樣的。之所以要存在編譯的過程,主要是為了優化執行的速度,比如元組,或者函數里面出現了yield,這一點在編譯的時候就已經確定了,編譯的時候就已經知道這是一個什么樣的數據結構,那么在執行的時候可以很快速的分配相應的內存。我們在打開python文件所在的目錄的時候,總會看到一個__pycache__的文件夾,這里面存放的就是python編譯之后的字節碼。當執行python文件的時候,會檢測當前的__pycache__目錄中是否有對應的字節碼,沒有就創建,有的話比較字節碼的創建時間和當前py文件的修改時間,如果字節碼的創建時間要晚一些,說明用戶沒有修改文件,於是執行字節碼,如果字節碼的創建時間要早一些,說明用戶修改了python源代碼,那么就會從新編譯得到一個新的字節碼。此外編譯還有一個重要的特點,就是語法檢測。錯誤分為兩種:一種是語法錯誤,另一種是邏輯錯誤。
-
語法錯誤就是源代碼沒有遵循python的規范,比如if判斷使用了一個=,或者for循環后面沒有:等等,這些都是屬於語法錯誤,這是一種低級的錯誤,在編譯的時候就會失敗。
try: > except Exception: pass """ 這個代碼是編譯不過去的,即便你使用了try···except。 語法錯誤就是不遵循python規范,編譯的時候都編譯不過。 """
-
那么另一種錯誤就是邏輯錯誤,這是語法沒問題,但是執行的時候出錯了,比如索引越界、和0相除、變量沒有定義等等,這些錯誤是在運行的時候才會出現的,這是可以被捕獲的。
try: a except Exception: pass # 這段代碼是不會報錯的。
python如何編譯py文件生成字節碼
python中的字節碼有兩種,pyc和pyo,兩者本質上沒啥區別,只不過pyo的優化程度更高一些。
編譯可以通過py_compile模塊進行編譯
# test.py
deffoo(name):
print("hello " + name)
我們來對test.py進行編譯
import py_compile
""" 參數如下: file:要編譯的py文件 cfile:編譯之后的字節碼文件,不指定的話默認為源文件目錄下的__pycache__目錄的下的'源文件名.解釋器類型-python版本.字節碼類型'文件 dfile:錯誤消息文件,默認和cfile一樣,一般不用管 doraise:是否開啟異常處理,默認和False optimize:優化字節碼級別。如果是pyc:可以選-1或0。pyo的話,可以選1或2。都是值越小優化程度越高 """
py_compile.compile(file="test.py", cfile=r"./test.pyc", optimize=-1)
py_compile.compile(file="test.py", cfile=r"./test.pyo", optimize=1)
可以看到,已經編譯成功了,pyc是可以直接當做普通py文件導入的,但是pyo貌似不可以,所以一般我們只編譯成pyc形式的字節碼。但是如果不導入只是執行的話,那么是可以編譯成pyo的。
import test
test.foo("mashiro") # hello mashiro
編譯的另一種方式,我們也可以直接使用命令行。
編譯成pyc
python -m py_compile 源代碼
編譯成pyo
python -O -m py_compile 源代碼
如果需要編譯整個目錄內的所有源代碼
python compileall
編譯成pyd文件
這個pyd實際上就是Windows上的dll文件,但是pyd是由py文件生成的,是可以直接當成python模塊導入的。而dll的話一般是c或者c++編寫的擴展模塊,這個時候我們會使用ctypes進行加載,后面會說。而Windows的pyd在linux上面則是so文件,dll在linux上面也是so文件,這個時候是使用ctypes還是使用普通加載模塊的方式,就看具體情況了。
我們下面測試一段python代碼,看看會用多長時間,然后將其編譯成pyd之后再測試一下。
# test_v.py
deffunc():
for _ in range(10000):
sum = 0
for i in range(100000):
sum += i
可以看到我們將sum依次從0加到100000-1,然后重復這個過程10000次,我們來測試一下用了多長時間。
import time
from test_v import func
start = time.perf_counter()
func()
end = time.perf_counter()
print("總耗時:", end - start) # 總耗時: 45.554086
直接導入py文件,調用函數執行,總共花了45秒鍾,下面我們來編譯成pyd。
那么如何編譯成pyd呢?
首先確保電腦上安裝了64位的MinGW,然后安裝cython,pip install cython
,新建一個py文件to_pyd.py
。
# to_pyd.py
# 導入模塊
import Cython.Build
# 傳入要編譯成pyd的py文件
ext = Cython.Build.cythonize("test_v.py")
# 下面還要導入另一個模塊
import distutils.core
# 調用setup方法
distutils.core.setup(
ext_modules=ext, # 將Cython.Build.cythonize返回的結果傳進去
)
然后在命令行輸入python to_pyd.py build
,即可把py文件test_v.py
編譯成pyd。執行之后,會得到一個對應的test_v.c
文件,以及一個build目錄。這個生成的c文件我們不需要管,我們看看build目錄。
我們看到此時就得到了對應的pyd文件,也叫test_v
,后面的則是python的版本號以及操作系統類型、位數等等,我們來測試一下性能吧。只把那個pyd文件拿出來,其他沒用的都刪掉,
import time
import test_v
# 我們看到導入之后,顯示的是pyd
print(test_v) # <module 'test_v' from 'C:\\Users\\satori\\Desktop\\love_minami\\test_v.cp38-win_amd64.pyd'>
start = time.perf_counter()
test_v.func()
end = time.perf_counter()
print("總耗時:", end - start) # 總耗時: 12.3021872
此時我們驚奇地看到,用了12秒,確實快了不少。主要是cython將python代碼進行了優化,另外編譯成pyd之后,是很難再反編譯成py文件的,如果你的模塊必須開源但是又不想被人看到某些細節的話,那么就可以編譯成pyd。對於字節碼pyc文件的反編譯已經有人實現了,可以將pyc轉成py文件,但是pyd目前還沒有被反編譯過。
那為什么編譯成pyd的時候速度會提升呢?主要是cython將代碼進行了優化,轉化成了c一級的代碼。另外我們說,test_v.cp38-win_amd64.pyd
里面的38就是解釋器的版本,我們這里是python3.8。這樣的話,也就意味着只有當你的版本是python3.8的時候,才會去導入這個模塊,於是我們把中間那一串給刪掉只保留test_v.pyd
可不可以呢?我們可以試一下
import test_v
print(test_v) # <module 'test_v' from 'C:\\Users\\satori\\Desktop\\love_minami\\test_v.pyd'>
事實證明確實是可以的,另外這樣的話不光是python3.8,其他版本的python也是可以導入的,只要編譯成pyd所使用的py文件,符合執行的python解釋器的語法規范即可。
python結合c語言
我們說使用cython確實能夠加速代碼,但肯定還是沒有原生的c語言執行的快。我們將上面的代碼轉換成c的代碼來測試一下,進而引入如何將python和c進行結合。
//1.c
longlongfunc(){
int _;
long long sum;
long i;
for (_ = 0;_ < 10000; _ ++)
{
sum = 0;
for (i = 0;i < 100000; i++)
{
sum += i;
}
}
return sum;
}
然后我們將這個1.c
編譯成dll,在linux中就是so,通過命令gcc -o 編譯之后的dll或者so文件名 -shared c源文件
編譯。
我們這里就編譯成mmp.dll吧:所以是gcc -o mmp.dll -shared 1.c
可以看到mmp.dll
已經出現了, 下面就來調用它
import time
import ctypes
# 調用ctypes.cdll.LoadLibrary,傳入dll的路徑
# 這個方法就等價於dll = ctypes.CDLL("xxx.dll"),用哪種都行,但是要求dll或者so的路徑是絕對路徑
# 另外這兩種方式在Windows上加載dll和linux上加載so都是可以的。
dll = ctypes.cdll.LoadLibrary(r"C:\Users\satori\Desktop\love_minami\mmp.dll")
start = time.perf_counter()
# 此時把dll看成一個模塊即可,里面定義了很多函數,比如func
dll.func()
end = time.perf_counter()
print("總耗時:", end - start) # 總耗時: 2.3377831
可以看到用時不到3秒,而我使用原生的python執行需要45秒,使用cython加速也需要12秒。首先我必須指出,當sum依次從0加到100000-1時,long long存不下。但是相同功能的程序,c的速度肯定會比cython編譯的pyd快,這一點可以自己測試一下,我這里就不再試了。
ctypes類型和c語言類型
我們直接調用一個函數顯然是沒有問題的,但如果函數里面需要參數呢?我們還能直接傳遞python的原生類型嗎?
//計算兩個數之和
intadd(int a, long b){
int sum;
sum = a + b;
return sum;
}
//查找指定字符在字符串中出現的位置
intfind_pos(char *string, char subchar){
char *p;
int pos = 0;
for (p = string; *p != '\0'; p++, pos++){
if (*p == subchar)
{
return pos;
}
}
return -1;
}
import ctypes
dll = ctypes.cdll.LoadLibrary(r"C:\Users\satori\Desktop\love_minami\mmp.dll")
print(dll.add(100, 200)) # 300
print(dll.find_pos("satori", "a")) # -1
我們看到對於整型來說是沒有問題的,但是對於字符串就有問題了,因為c中沒有字符串的概念,這時候應該怎么做呢?
import ctypes
from ctypes import c_char_p, c_char
dll = ctypes.cdll.LoadLibrary(r"C:\Users\satori\Desktop\love_minami\mmp.dll")
# c語言中沒有字符串這個概念,c語言中的字符串實際上是字符數組,c的這些概念不再介紹
# 傳遞一個指向字符數組的指針,同理字符a不能直接傳遞,需要使用c_char包裝一下,並且里面需要傳遞字節。
print(dll.find_pos(c_char_p(b"satori"), c_char(b"a"))) # 1
所以我們來看看ctypes給我們提供了哪些類型,這些類型又對應c中的哪些類型呢?
from ctypes import *
print(c_int(1)) # c_long(1)
print(c_uint(1)) # c_ulong(1)
print(c_short(1)) # c_short(1)
print(c_ushort(1)) # c_ushort(1)
print(c_long(1)) # c_long(1)
print(c_ulong(1)) # c_ulong(1)
print(c_longlong(1)) # c_longlong(1)
print(c_ulonglong(1)) # c_ulonglong(1)
print(c_float(1.1)) # c_float(1.100000023841858)
print(c_double(1.1)) # c_double(1.1)
# 在64位機器上,c_longdouble等於c_double
print(c_longdouble(1.1)) # c_double(1.1)
print(c_bool(True)) # c_bool(True)
# 必須傳遞一個字節或者只有一個元素的字符數組,或者一個int
# 代表c里面的字符
print(c_char(b"a"), c_char(bytearray(b"x"))) # c_char(b'a') c_char(b'x')
# 傳遞一個unicode字符
print(c_wchar("憨")) # c_wchar('憨')
# 和c_char類似,但是要求傳遞一個整型
print(c_byte(97)) # c_byte(97)
print(c_ubyte(97)) # c_ubyte(97)
# c_char_p就是c里面字符數組指針了
# char *s = "hello world";
# 那么這里面也要傳遞一個字符數組,字符是bytes類型,返回一個地址
print(c_char_p(b"hello world")) # c_char_p(2082736374464)
# 直接傳遞一個unicode,同樣返回一個地址
print(c_wchar_p("憨八嘎~")) # c_wchar_p(2884583039392)
# 並且還有一個c_size_t和c_ssize_t
# 相當於c_ulonglong和c_longlong,這個和機器有關
print(c_size_t(10)) # c_ulonglong(10)
print(c_ssize_t(10)) # c_longlong(10)
當然c中各種類型,在ctypes都有對應。比如我們沒有介紹的結構體等等,更復雜的用法可以參考官網。