python如何編譯py文件生成pyc、pyo、pyd以及如何和C語言結合使用


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都有對應。比如我們沒有介紹的結構體等等,更復雜的用法可以參考官網。

 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM