至今學習 Cython 語言的筆記。

此篇文章可能需要一些 C 語言基礎。

前言

Cython 是 Python 的超集 (superset) 語言,直接將 Python 腳本交給 Cython 進行編譯是可行的;正如 Type script 與 Java script 的關係。

Cython 中能夠使用 Python 全部的語法,並且支援額外模組的匯入。不過須注意的是,Cython 會不斷參照 PEP (Python Enhancement Proposals) 規則進行強化,以兼顧 Python 的撰寫風格,因此推薦升級模組時跟進 Python 版本。

Python 語法概要

之前提過的 記憶體管理 方法,為方便再簡要介紹。

建立與刪除

Python 使用「名稱綁定」的方式指派變數。

a = 10 #將記憶體中儲存數值 10,並給予名稱 "a"。
b = 10 #找到已儲存數值 10,再給予名稱 "b"。
print(a is b) #True,"a" 與 "b" 共用記憶體。

del a #尋找並移除名稱 "a",10 仍存於記憶體。
del b #尋找並移除名稱 "b",10 失去所有名稱,被刪除。

修改

使用重新綁定以使名稱中的值變化。

a = 10
b = 10
b += 1 #針對 "b" 名稱取值並進行加法運算,得 2。

"""此時 "b" 名稱重新綁定到新結果 2。"""

print(a is b) #False,"a" 與 "b" 使用不同記憶體。

複製與參照

作用域更改時(導入模組、進入類型、進出函式等),物件將會改名,前方編上作用域名稱。

Python 遵循以下方法:

  • 字面類型 (literal) 包含 bool、Numeric (int, float, complex)、str 共五種,使用複製

  • 其餘類型如容器 (container)、函式 (function)、自訂類型等,則為參照

  • 複製行為是創造副本,對複本修改只能在當前作用域有效(例如函式中),離開後此改動值會失去名稱,也不會存回本體。

  • 參照相當於取別名,此名稱執行任何行為可以影響本體。

由於多了一層「名稱綁定」,Python 的執行速度就不會如靜態語言如此高效。

Cython 語法概要

Python 的超高效開發速度就是因為簡單有效的名稱運用,而不必注意瑣碎的運算子操作。

當需要對 Python 程式提速時,將名稱轉為靜態類型就可以了,剩下的就如 C 語言一樣交由編譯器規劃。

直接對 Python 腳本編譯,會稍微進行提速,這得感謝你安裝的 C 語言編譯器。通常編譯器會試著找到 Python 對於名稱嘮嘮叨叨之處,試圖簡化語法。

但是越大的腳本,名稱網路就會越複雜,效果將會更不明顯。

延伸語法:C 語言名稱

接下來介紹全新的 Cython 關鍵字 cdef

cdef int a = 0 #宣告靜態整數類型 "a",規劃記憶體存入 0 值。
cdef a = 0 #建立靜態 "a" 指派 0?Cython 會自動推斷 "a" 的型別。

此變數會完全參照 C 語言的行為,而非靜態變數則會參照 Python 的「名稱綁定」。

同一作用域下,名稱更改時,若沒有指標或參照運算子操作,一律為複製

不同作用域時,則同於 Python

cdef int a = 10
b = a #將整數綁定名稱 "b"。
cdef int c = b #將名稱 "b" 的值轉型為整數,複製到名稱 "c"。
print(a is b) #True,兩者共用記憶體。
print(a is c) #False,兩者不共用記憶體。

跟 C 語言一樣,使用此關鍵字的名稱「宣告」與「定義」是可以分開的。

cdef int a, b, c #宣告三個整數類型的名稱 "a"、"b"、"c"。
a = 20 #對 "a" 指派 20 完成定義。
for b in [5, 4, 6, 8]: #名稱 "b" 將用於取出迭代值。
    if b > 5:
        c = func(b*b) #名稱 "c" 將用於判斷式區塊中。

即使是函式或類型也可以,當然參數 (arguments) 的簽章 (signature) 必須一致。

cdef int add(int, int b = *) #宣告新函式

cdef inline int add(int a, int b = 10): #完成定義函式(可再加上簡單的修飾符)
    return a + b

cdef class NewClass(OldClass) #宣告新類型

cdef class NewClass(OldClass): #完成定義類型
    pass

經過靜態定義的類型就像有一部份的 C 語言血統,部分功能也能加速或改變性能,可以參照這裡:

混和 C 和 C++

而 C 語言的類型則可以完全支援,如使用標頭檔引入 C 類型:

enum class Color {red, green = 20, blue}; 

接著透過 extern from ... : 語句建立型別檢查器來引入物件:

cdef extern from "colors.h": #引入類型作為檢查器。
  cdef cppclass Color:
    pass

cdef extern from "colors.h" namespace "Color": #引入數值。
  cdef Color red
  cdef Color green
  cdef Color blue

cdef class PyColor: #給 Python 透過類型方法取值。
  cdef Color thisobj

  def __cinit__(self, int val):
    self.thisobj = <Color> val

  def get_color_type(self):
    cdef dict c = {<int>red : "red", <int> green : "green", <int> blue : "blue"}
    return c[<int>self.thisobj]

當然這樣可以直接在 Cython 中寫 C 或 C++ 大部分的語法,但是如果這樣的話效能會減損很多,C 語言的東西還是先自己包好再引導給 Python 吧。

端口

必須注意的是,Python 無法從程式庫中取出靜態類型的名稱(除了類型的名稱),想要取得必須透過幾種方式:

  • 函式回傳。
  • 使用修飾符。

兩種函式可以給 Python 看到:

cpdef void func_cpy(int a): #使用 C 語言製作的函式接口。
    print("P{}".format(a))

def func_py(a: int) -> None: #使用 C 物件的純 Python 函式。
    print("P{}".format(a))

cdefcpdefdef 三種函式中,速度最快自然是使用 cdef 生成的函式,Python 的 def 函式是最慢的。

不過兩種有端口的函式使用上自然有限制,例如完全不支援修飾符。

不過,Cython 可以支援使用 def 生成 Python 生產器 (generator),並且能很好的使用靜態類型的物件,至於效率自然是會因為同步迭代而提升,就視情況自行取捨。

順帶一提,Cython 編譯時也能簡單檢查 Python 的 typing 語法,不過不支援從 typing 模組匯入的類型。

使用 readonlypublic 修飾符可以使物件被 Python 可視,不過仍有些許限制,只能給予 Python 支援的類型,指標、陣列或 vector 容器自然是不可能暴露給 Python。

標頭檔 pxd

由於宣告與定義可以分開,假如使用 from ... import ... 語句,只能以 Python 的方式引入。不但得經過語言端口,C 語言的名稱會完全看不到。因此在 Cython 程式庫之間設計了新的語句 from ... cimport ...,達成兩個 pyx 共用程式的效果。

鑒於 C 和 C++ 的原理相仿,相同宣告的名稱使用相同的定義,這邊就不多作介紹了,基本上這句 cimport 可以當作有 namespace 遮罩的 include 語法。

Cython 比較不一樣的是,為了避免混淆名稱,擁有該 pxd 定義的 pyx 程式庫,兩者必須同名定義不可分割,而此 pyx 程式庫不用任何 cimport 自該 pxd 的語句。

這樣,其他 pyx 程式庫就可以藉由 pxd 的宣告項目進行 cimport。

自訂檢查器

使用 ctypedef fused ... : 語句建立客製化的檢查器,讓多種類型以供選擇。

ctypedef fused sequence:
    list
    tuple

此表現類似於 C++ 的 template 功能。


Comments

comments powered by Disqus