久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

編譯并運行 Cython 代碼的幾種方式

 古明地覺O_o 2022-12-08 發(fā)布于北京

楔子


Python 和 C,、C++ 之間一個最重要的差異就是 Python 是解釋型語言,而 C,、C++ 是編譯型語言,。如果開發(fā) Python 程序,那么在修改代碼之后可以立刻運行,,而 C,、C++ 則需要一個編譯步驟。編譯一個規(guī)模比較大的 C,、C++ 程序,,可能會花費我們幾個小時的時間;而使用 Python 則可以讓我們進行更敏捷的開發(fā),,從而更具有生產(chǎn)效率,。

所以在開發(fā)游戲的時候,,都會引入類似 Lua,、Python 之類的腳本語言。特別是手游,,腳本語言是必不可少的,。

而 Cython 同 C、C++ 類似,,在源代碼運行之前也需要一個編譯的步驟,,不過這個編譯可以是顯式的,,也可以是隱式的。如果是顯式,,那么在使用之前需要提前手動編譯好,;如果是隱式,那么會在使用的時候自動編譯,。 

而自動編譯 Cython 的一個很棒的特性就是它使用起來和純 Python 是差不多的,,但無論是顯式還是隱式,我們都可以將 Python 的一部分(計算密集)使用 Cython 重寫,。因此 Cython 的編譯需求可以達到最小化,,沒有必要將所有的代碼都用 Cython 編寫,而是將那些需要優(yōu)化的代碼使用 Cython 編寫即可,。

那么本次就來介紹編譯 Cython 代碼的幾種方式,,并結合 Python 使用。因為我們說 Cython 是為 Python 提供擴展模塊,,最終還是要通過 Python 解釋器來調用的,。

而編譯Cython有以下幾個選擇:

  • Cython 代碼可以在 IPython 解釋器中進行編譯,并交互式運行,;

  • Cython 代碼可以在導入的時候自動編譯,;

  • Cython 代碼可以通過類似于 Python 內置模塊 disutils 的編譯工具進行獨立編譯;

  • Cython代碼可以被繼承到標準的編譯系統(tǒng),,例如:make,、CMake、SCons,;

這些選擇可以讓我們在幾個特定的場景應用 Cython,,從一端的快速交互式,探索到另一端的快速構建,。

但無論是哪一種編譯方式,,從 Cython 代碼到 Python 可以導入和使用的擴展模塊都需要經(jīng)歷兩個步驟。在我們討論每種編譯方式的細節(jié)之前,,需要了解一下這兩個步驟到底在做些什么,。


編譯步驟


因為 Cython 是 Python 的超集,所以 Python 解釋器無法直接運行 Cython 代碼,,那么如何才能將 Cython 代碼變成 Python 解釋器可以識別的有效代碼呢,?

1)由 Cython 編譯器負責將 Cython 代碼轉換成經(jīng)過優(yōu)化并且依賴當前平臺的 C 代碼;

2)使用標準 C 編譯器將第一步得到的 C 代碼進行編譯并生成標準的擴展模塊,,并且這個擴展模塊是依賴特定平臺的,。如果是 Linux 或者 Mac OS,那么得到的擴展模塊的后綴名為 .so,如果是 Windows ,,那么得到的擴展模塊的后綴名為 .pyd(本質上是一個 DLL 文件),。

不管是什么平臺,最終得到的都會是一個成熟的 Python 擴展模塊,,它是可以直接被 Python 解釋器識別并 import 的,。

Cython 編譯器是一種源到源的編譯器,并且生成的擴展模塊也是經(jīng)過高度優(yōu)化的,,因此 Cython 生成的 C 代碼編譯得到的擴展模塊,, 比手寫的 C 代碼編譯得到的擴展模塊運行的要快,并不是一件稀奇的事情,。因為 Cython 生成的 C 代碼經(jīng)過高度精煉,,所以大部分情況下比手寫所使用的算法更優(yōu),而且 Cython 生成的 C 代碼支持所有的通用 C 編譯器,,生成的擴展模塊同時支持許多不同的 Python 版本,。

所以 Cython 和 C 擴展本質上干的事情是一樣的,都是將符合 Python/C API 的 C 代碼編譯成 Python 擴展模塊,。只不過寫 Cython 的話,,我們不需要直接面對 C,Cython 編譯器會自動將 Cython 代碼翻譯成 C 代碼,,然后我們再將其編譯成擴展模塊,。

所以兩者本質是一樣的,只不過 C 比較復雜,,而且難編程,;但是 Cython 簡單,語法本來就和 Python 很相似,,所以我們選擇編寫 Cython,,然后讓 Cython 編譯器幫我們把 Cython 代碼翻譯成 C 的代碼。而且重點是得到的 C 代碼是經(jīng)過優(yōu)化的,,如果我們能寫出很棒的 Cython 代碼,,那么也會得到同樣高質量的 C 代碼。


安裝環(huán)境


編譯 Cython 代碼有兩個步驟:先將它翻譯成 C 代碼,,然后將 C 代碼編譯成擴展模塊,。而實現(xiàn)這兩個步驟需要我們確保機器上有 C 編譯器以及 Cython 編譯器,不同的平臺有不同的選擇,。

C 編譯器

Linux 和 Mac OS 無需多說,,因為它們都自帶 gcc,但是注意:如果是 Linux 的話,,我們還需要 yum install python3-devel(以 CentOS 為例),。

至于 Windows,可以下載一個 Visual Studio,,但是那個玩意比較大,。如果不想下載 vs 的話,那么可以選擇安裝一個 MinGW 并設置到環(huán)境變量中,,至于下載方式可以去官網(wǎng)進行下載,。

我這里已經(jīng)配置好了,包括 MinGW 和 Visual Studio,。

Cython 編譯器

安裝 Cython 編譯器的話,,直接 pip install Cython 即可。因此我們看到 Cython 編譯器只是 Python 的一個第三方包,,它的作用就是對 Cython 代碼進行解析,,然后生成 C 代碼。因此 Cython 編譯器想要運行,,同樣需要借助 CPython 解釋器,。

from Cython import __version__

print(__version__)  # 0.29.14

如果能夠正常執(zhí)行,那么證明安裝成功,。

disutils

有了 Cython 編譯器,,我們就可以生成 C 代碼了;有了 C 編譯器,,我們就能基于 C 代碼生成擴展模塊了,。但是第二步比較麻煩,因為要輸入的命令參數(shù)非常多,,而 Python 有一個標準庫 disutils,,專門用來構建、打包,、分發(fā) Python 工程,,可以方便我們編譯。

disutils 有一個對我們非常有用的特性,,就是它可以借助 C 編譯器將 C 源碼編譯成擴展模塊,,并且這個模塊是自帶的,考慮了平臺,、架構,、Python 版本等因素,因此我們在任意地方使用 disutils 都可以得到擴展模塊,。

那么廢話不多說,,下面就來看看如何編譯。


手動編譯 Cython 代碼


先來編寫 Cython 源文件,,還以斐波那契數(shù)列數(shù)列為例,,文件就叫 fib.pyx,。Cython 源文件的后綴,以 .pyx 結尾,。

def fib(n):
    """這是一個擴展模塊"""
    cdef int i
    cdef double a=0.0, b=1.0
    for i in range(n):
        a, b = a + b, a
    return a

然后我們對其進行編譯,,創(chuàng)建一個 setup.py,里面寫上編譯相關的代碼:

from distutils.core import setup
from Cython.Build import cythonize

# 我們說構建擴展模塊的過程分為兩步: 
# 1)將 Cython 代碼翻譯成 C 代碼; 
# 2)根據(jù) C 代碼生成擴展模塊
# 第一步要由 Cython 編譯器完成, 通過 cythonize; 
# 第二步要由 distutils 完成, 通過 distutils.core 下的 setup
setup(ext_modules=cythonize("fib.pyx", language_level=3))
# 里面還有一個參數(shù) language_level=3 
# 表示只需要兼容 Python3 即可,,而默認是 2 和 3 都兼容
# 如果你是 Python3 環(huán)境,,那么建議加上這個參數(shù)

# cythonize 負責將 Cython 代碼轉成 C 代碼
# 然后 setup 根據(jù) C 代碼生成擴展模塊

下面就可以進行編譯了,通過 python setup.py build 即可完成編譯,。

執(zhí)行完命令之后,,當前目錄會多出一個 build 目錄,里面的結構如圖所示,。重點是那個 fib.cp38-win_amd64.pyd 文件,,該文件就是根據(jù) fib.pyx 生成的擴展模塊,至于其它的可以直接刪掉了,。我們把這個文件單獨拿出來測試一下:

import fib
# 我們看到該 pyd 文件直接就被導入了
# 至于中間的 cp38-win_amd64 指的是解釋器版本,、操作系統(tǒng)等信息
print(fib) 
"""
<module 'fib' from 'D:\\satori\\fib.cp38-win_amd64.pyd'>
"""


# 我們在里面定義了一個 fib 函數(shù)
# fib.pyx 里面定義的函數(shù)在編譯成擴展模塊之后可以直接用
print(fib.fib(20))  
"""
6765.0
"""


# doc string
print(fib.fib.__doc__)  
"""
這是一個擴展模塊
"""

我們在 Linux 上再測試一下,代碼以及編譯方式都不需要改變,,并且生成的動態(tài)庫的位置也不變,。

>>> import fib
>>> fib
<module 'fib' from '/root/fib.cpython-36m-x86_64-linux-gnu.so'>
>>> exit()

我們看到依舊是可以導入的,只不過 Linux 上是 .so 的形式,,Windows 上是 .pyd。因此我們可以看出,,所謂 Python 的擴展模塊,,本質上就是當前操作系統(tǒng)上一個動態(tài)庫。只不過生成該動態(tài)庫的 C 源文件遵循標準的 Python/C API,,所以它是可以被解釋器識別,、直接通過 import 語句導入的,就像導入普通的 py 文件一樣,。

而對于其它的動態(tài)庫,,比如 Linux 中存在大量的動態(tài)庫(.so文件),而它們則不是由遵循標準 Python/C API 的 C 文件生成的,,所以此時再通過 import 導入,,解釋器就無法識別了。如果 Python 真的想調用這樣的動態(tài)庫,,則需要使用 ctypes,、cffi 等模塊。

另外在 Windows 環(huán)境,,編譯器可以使用 gcc 或者 vs,,那么問題來了,,在生成擴展時,要如何指定編譯器種類呢,?非常簡單,,可以在標準庫 distutils 的目錄下新建一個 distutils.cfg 文件,里面寫入如下內容:

[build]
compiler=mingw32 或者 msvc

mingw32 代表 gcc,,msvc 代表 vs。

引入 C 源文件

Cython 的一大特色就在于,,它還可以引入已有的 C 文件,,因為 Cython 同時理解 C 和 Python。如果已經(jīng)有現(xiàn)成的 C 庫,,那么 Cython 可以直接拿來用,。

我們舉個栗子:

// 文件名:cfib.h
// 定義一個函數(shù)聲明
double cfib(int n);  


// 文件名:cfib.c
// 函數(shù)體的實現(xiàn)
double cfib(int n) {
    int i;
    double a=0.0, b=1.0, tmp;
    for (i=0; i<n; ++i) {
        tmp = a; a = a + b; b = tmp;
    }
    return a;

目前已經(jīng)有 C 實現(xiàn)好的斐波那契函數(shù)了,那么在 Cython 里面要如何使用呢,?我們來編寫 Cython 文件,,文件名還是 fib.pyx。

# 通過 cdef extern from 導入頭文件
# 寫上要用的函數(shù)
cdef extern from "cfib.h":
    double cfib(int n)

# 然后 Cython 可以直接調用
def fib_with_c(n):
    """調用 C 編寫的斐波那契數(shù)列"""
    return cfib(n)

最后是編譯:

from distutils.core import setup, Extension
from Cython.Build import cythonize

"""
之前是直接往 cythonize 里面?zhèn)魅胍粋€文件名即可
但是現(xiàn)在我們傳入了一個 Extension 對象
通過 Extension 對象的方式可以實現(xiàn)更多功能

這里指定的 name 表示編譯之后的文件名
顯然編譯之后會得到 wrapper_cfib.cp38-win_amd64.pyd

如果是之前的方式, 那么得到的就是 fib.cp38-win_amd64.pyd
默認會和 .pyx 文件名保持一致, 這里我們可以自己指定

sources 則是代表源文件,,需要指定 .pyx 以及使用的 c 源文件
"""

ext = Extension(name="wrapper_cfib"
                sources=["fib.pyx""cfib.c"])
setup(ext_modules=cythonize(ext, language_level=3))

編譯之后,,進行調用:

import wrapper_cfib

print(wrapper_cfib.fib_with_c(20)) 
"""
6765.0
"""


print(wrapper_cfib.fib_with_c.__doc__)  
"""
調用 C 編寫的斐波那契數(shù)列
"""

成功調用了 C 編寫的斐波那契數(shù)列函數(shù),這里我們使用了一種新的創(chuàng)建擴展模塊的方法,,來總結一下,。

1)如果是單個 pyx 文件的話,那么直接通過 cythonize("xxx.pyx") 即可,。

2)如果 pyx 文件還引入了 C 文件,,那么 cythonize 里面需要指定一個 Extension 對象。參數(shù) name 是編譯之后的擴展模塊的名字,,參數(shù) sources 是編譯的源文件,,并且不光要指定 .pyx 文件,依賴的 C 文件同樣要指定,。

建議后續(xù)都使用第二種方式,,可定制性更強,而且我們之前使用的 cythonize("fib.pyx") 完全可以用 cythonize(Extension("fib", ["fib.pyx"])) 進行替代,。

關于使用 Cython 包裝 C,、C++ 代碼的更多細節(jié),我們會在后續(xù)系列中詳細介紹,,總之編譯的時候相應的源文件是不能少的,。


通過 IPython 動態(tài)交互 Cython


使用 distutils 編譯 Cython 可以讓我們控制每一步的執(zhí)行過程,但也意味著我們在使用之前必須要先經(jīng)過獨立的編譯,,不涉及到交互式,。而 Python 的一大特性就是交互式,,比如 IPython,所以需要想個法子讓 Cython 也支持交互式,,而實現(xiàn)的辦法就是魔法命令,。

我們打開 IPython,在上面演示一下,。

# 我們在 IPython 上運行
# 執(zhí)行%load_ext cython便會加載Cython的一些魔法函數(shù)
In [1]: %load_ext cython

# 然后神奇的一幕出現(xiàn)了
# 加上一個魔法命令,,就可以直接寫Cython代碼
In [2]: %%cython
   ...: def fib(int n):
   ...:     """這是一個 Cython 函數(shù),在 IPython 上編寫"""
   ...:     cdef int i
   ...:     cdef double a = 0.0, b = 1.0
   ...:     for i in range(n):
   ...:         a, b = a + b, a
   ...:     return a

# 測試用時,,平均花費82.6ns
In [6]: %timeit fib(50)
82.6 ns ± 0.677 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

注意:以上同樣涉及到編譯成擴展模塊的過程,。

首先 IPython 中存在一些魔法命令,這些命令以一個或兩個百分號開頭,,它們提供了普通 Python 解釋器所不能提供的功能,。%load_ext cython 會加載 Cython 的一些魔法函數(shù),如果執(zhí)行成功將不會有任何的輸出,。

然后重點來了,,%%cython 允許我們在 IPython 解釋器中直接編寫 Cython 代碼,當我們按下兩次回車時,,顯然這個代碼塊就結束了,。但是里面的 Cython 代碼會被 copy 到名字唯一的 .pyx 文件中,并將其編譯成擴展模塊,,編譯成功之后 IPython 會再將該模塊內的所有內容導入到當前的環(huán)境中,,以便我們使用。

因此上述的編譯過程,、編譯完成之后的導入過程,,都是我們在按下兩次回車鍵之后自動發(fā)生的。但是不管怎么樣,,它都涉及到編譯成擴展模塊的過程,,包括后面要說的即時編譯也是如此,只不過這一步不需要手動做了,。

當然相比 IPython,,我們更常用 jupyter notbook,既然 Cython 在前者中可以使用,,那么后者肯定也是可以的,。

jupyter notebook 底層也是使用了 IPython,所以它的原理和 IPython 是等價的,,會先將代碼塊 copy 到名字唯一的 .pyx 文件中,,然后進行編譯。編譯完畢之后再將里面的內容導入進來,,而第二次編譯的時候由于單元格里面的內容沒有變化,,所以不再進行編譯了,。

另外在編譯的時候如果指定了 --annotate 選項,那么還可以看到對應的代碼分析,。

可以看到還是非常強大的,,尤其是在和 jupyter 結合之后,真的非常方便,。


使用 pyximport 即時編譯


因為 Cython 是以 Python 為中心的,,所以我們希望 Python 解釋器在導包的時候能夠自動識別 Cython 文件,導入 Cython 就像導入常規(guī),、動態(tài)的 Python 文件一樣,。但是不好意思,Python 在導包的時候并不會自動識別以 .pyx 結尾的文件,,但是我們可以通過 pyximport 來改變這一點,。

pyximport 也是一個第三方模塊,,安裝 Cython 的時候會自動安裝,。

def fib(int n):
    cdef int i
    cdef double a = 0.0, b = 1.0
    for i in range(n):
        a, b = a + b, a
    return a

文件名仍叫 fib.pyx,下面來導入它,。

import pyximport
# 這里同樣指定 language_level=3
# 表示針對的是 py3
pyximport.install(language_level=3)
# 執(zhí)行完之后, 解釋器在導包的時候就會識別 Cython 文件了
# 當然這個過程也是需要先編譯的

import fib
print(fib.fib(20))  # 6765.0

正如我們上面演示的那樣,,使用 pyximport 可以讓我們省去 cythonize 和 distutils 這兩個步驟(注意:這兩個步驟還是存在的,只是不用我們做了),。

另外 Cython 源文件不會立刻編譯,,只有當被導入的時候才會編譯。即便后續(xù) Cython 源文件被修改了,,pyximport 也會自動檢測,,當重新導入的時候也會再度重新編譯,機制就和 Python 的 pyc 文件是一個道理,。

自動編譯之后的 pyd 文件位于 ~/.pyxbld/lib.xxx 中,。

但是這樣有一個弊端,我們說 pyx 文件并不是直接導入的,,而是在導入之前先有一個編譯成擴展模塊的步驟,,然后導入的是這個擴展模塊,只不過這一步驟不需要我們手動來做了,。

所以它要求你的當前環(huán)境中有一個 Cython 編譯器以及合適的 C 編譯器,,而這些環(huán)境是不受控制的,沒準哪天就編譯失敗了,。因此最保險的方式還是使用我們之前說的 distutils,,先編譯成擴展模塊(.pyd 或者 .so),然后再放在生產(chǎn)模式中使用,。

但是問題來了,,如果 Cython 文件中還引入了其它的 C 文件該怎么辦呢,?還以我們之前的斐波那契數(shù)列為例:

// 文件名:cfib.h
// 定義一個函數(shù)聲明
double cfib(int n);  


// 文件名:cfib.c
// 函數(shù)體的實現(xiàn)
double cfib(int n) {
    int i;
    double a=0.0, b=1.0, tmp;
    for (i=0; i<n; ++i) {
        tmp = a; a = a + b; b = tmp;
    }
    return a;

然后是 fib.pyx 文件。

def extern from "cfib.h":
    double cfib(int n)

def fib_with_c(n):
    return cfib(n)

那么問題來了,,如果這個時候通過 pyximport 來導入 fib 會發(fā)生什么后果呢,?答案是報錯,因為它不知道該去哪里尋找這些外部文件,,而顯然這些文件應該是要鏈接在一起的,。那么要如何做呢?就是我們下面要說的問題了,。


控制 pyximport 并管理依賴


我們說手動編譯的時候,,需要指定依賴的 C 文件的位置,但是直接導入 .pyx 文件的時候就不知道這些依賴在哪里了,。所以我們應該還要定義一個 .pyxbld 文件,,.pyxbld 文件要和 .pyx 文件具有相同的基名,比如我們是為了指定 fib.pyx 文件的依賴,,那么 .pyxbld 文件就應該叫做 fib.pyxbld,,并且它們要位于同一目錄中。

那么這個 fib.pyxbld 文件里面應該寫什么內容呢,?

# fib.pyxbld
from distutils.extension import Extension

def make_ext(modname, pyxfilename):
    """
    如果 .pyxbld 文件中定義了這個函數(shù)
    那么在編譯之前會進行調用,,并自動往進行傳參
    modname 是編譯之后的擴展模塊名,顯然這里就是 fib
    pyxfilename 是編譯的 .pyx 文件,,顯然是 fib.pyx
    注意: .pyx 和 .pyxbld 要具有相同的基名稱
    然后它要返回一個我們之前說的 Extension 對象
    :param modname:
    :param pyxfilename:
    :return:
    """

    return Extension(modname,
                     sources=[pyxfilename, "cfib.c"],
                     # include_dir 表示在當前目錄中尋找頭文件
                     include_dirs=["."])
    # 我們看到整體還是類似的邏輯,,因為編譯這一步是怎么也繞不過去的
    # 區(qū)別就是手動編譯還是自動編譯,如果是自動編譯,,顯然限制會比較多
    # 想解除限制,,需要定義 .pyxbld 文件
    # 但很明顯,這和手動編譯沒啥區(qū)別了

此時我們再來直接導入看看,,會不會得到正確的結果,。

import pyximport
pyximport.install(language_level=3)

import fib
print(fib.fib_with_c(50))
"""
12586269025.0
"""

.pyxbld 文件中除了通過定義 make_ext 函數(shù)的方式外,還可以定義 make_setup_args 函數(shù),。對于 make_ext 函數(shù),,在編譯的時候會自動傳遞兩個參數(shù):modname 和 pyxfilename。但如果定義的是 make_setup_args 函數(shù),,那么在編譯時不會傳遞任何參數(shù),,一些都由你自己決定。

但這里還有一個問題,,首先 Cython 源文件一旦改變了,,那么再導入的時候就會重新編譯;但如果 Cython 源文件(.pyx)依賴的 C 文件改變了呢?這個時候導入的話還會自動重新編譯嗎,?答案是會的,,Cython 編譯器不僅會檢測 Cython 文件的變化,還會檢測它依賴的 C 文件的變化,。

我們將 fib.c 中的函數(shù) cfib 的返回值加上 1.1,,然后其它條件不變,看看結果如何,。

import pyximport
pyximport.install(language_level=3)

import fib
print(fib.fib_with_c(50))  
"""
12586269026.1
"""

可以看到結果變了,,之前的話還需要定義一個具有相同基名的 .pyxdeps 文件,來指定 .pyx 文件具有哪些依賴,,但是目前不需要了,,會自動檢測依賴文件的變化。

但是說實話,,像這種依賴 C 文件的情況,,建議還是事先編譯好,這樣才能百分百穩(wěn)定運行,。當然如果你部署服務的環(huán)境具備編譯條件,,那么也可以不用提前編譯。


小結


目前我們介紹了如何將 pyx 文件編譯成擴展模塊,,對于一個簡單的 pyx 文件來說,,方法如下:

from distutils.core import setup, Extension
from Cython.Build import cythonize

# 推薦以后就使用這種方法
ext = Extension(
    # 生成的擴展模塊的名字
    name="wrapper_fib",  
    # 源文件
    sources=["fib.pyx", "cfib.c"], 
)
setup(ext_modules=cythonize(ext, language_level=3))

如果還依賴 C 文件,,那么就在 sources 參數(shù)里面把依賴的 C 文件寫上即可,。另外,如果你在編譯時發(fā)現(xiàn)報錯,,找不到相應的頭文件,、C 源文件,那么說明你的查找目錄沒有指定正確,。關于這一方面我們后續(xù)再聊,。

此外還可以通過 pyximport 自動編譯,我們后面在學習 Cython 語法的時候,,就采用這種自動編譯的方式了,。因為方便,不需要我們每次都來手動編譯,,但是要將服務放在生產(chǎn)環(huán)境中,,建議還是提前編譯好。

    轉藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多