Python源碼漫遊指南(一)

Python源碼漫遊指南(一)

做者:祕塔科技算法研究員 Qian Wan前端

前幾天IEEE Spectrum發佈了第五屆頂級語言交互排行榜,Python語言繼續穩坐第一把交椅,而且相比去年的排行狀況,拉開了與第二名的距離(去年第二名的排名得分爲99.7)。從下圖能看出Python的優點仍是很明顯的,並且在Web、企業級和嵌入式這三種應用類別的流行度都很高。python

clipboard.png

冰凍三尺非一日之寒。Python語言自1990年由Guido van Rossum第一次發佈至今已經快三十年的歷史,它支持多種操做系統,並以CPython爲參考實現。Python語言在不少領域都有殺手級的應用框架,如深度學習方面有PyTorch和Tensorflow,天然語言處理有NLTK,Web框架有Django、Flask,科學計算有Numpy、Scipy,計算機視覺有OpenCV,科學繪圖有Matplotlib,爬蟲有Scrapy,凡此種種,不一而足。面對這麼多不一樣種類的Python應用框架,下面一些問題是值得咱們思考的:git

  1. 怎樣使用Python語言能將程序的性能發揮到極致?
  2. 什麼類型的單一語言框架不適合用Python來實現?
  3. 多語言框架中與Python語言的交互如何作到高效?
  4. 從架構的角度看,Python內部的架構設計如何?
  5. 從使用Python語言的角度,它適合於什麼樣的軟件架構設計?
  6. 在多語言(Python與CUDA)、異構節點(CPU與GPU)、多業務類型(IO密集型與CPU密集型)以及跨區域(跨國多機房)的複雜系統中,Python語言的定位又如何?其餘語言呢?

三言兩語可能很難比較全面的回答上面一些問題,並且只研究Python語言獲得的答案也可能會有失偏頗。可是Python語言的源代碼可以爲回答這些問題提供一些線索,並且經過閱讀源碼能讓咱們在使用Python語言時看到一些之前咱們看不到的細節,就如同《黑客帝國》電影裏的Neo同樣能看到母體世界的源代碼,也能像Neo那樣在機器的世界裏飛天遁地。github

Python環境的部署

咱們使用pyenv花幾分鐘時間來構建Python運行環境,它不只能夠與操做系統原生的Python環境隔離,還能支持多種版本的Python環境,另外也支持在同一Python版本下的多個虛擬環境,能夠用來隔離不一樣應用的Python依賴包。部署代碼以下算法

$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
$ git clone https://github.com/pyenv/pyenv-virtualenv.git ${HOME}/.pyenv/plugins/pyenv-virtualenv
$ echo 'eval "$(pyenv init -)"' >> ~/.bashrc
$ echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc
$ CONFIGURE_OPTS=--enable-shared $HOME/.pyenv/bin/pyenv install 3.6.6 -k -v
$ $HOME/.pyenv/bin/pyenv virtualenv 3.6.6 py3.6

部署好了以後每次運行下面命令就能替換掉系統原生的Python環境緩存

$ pyenv activate py3.6

安裝後的目錄結構以下sass

  • Python源碼:~/.pyenv/sources/3.6.6/Python-3.6.6
  • 頭文件:~/.pyenv/versions/3.6.6/include/python3.6m/
  • 動態連接庫:~/.pyenv/versions/3.6.6/lib/libpython3.6m.dylib

目錄結構

要深刻剖析Python的源代碼,就要對源碼中幾個大的模塊的做用有一個初步的認識。咱們進入到源碼目錄~/.pyenv/sources/3.6.6/Python-3.6.6,其中幾個跟Python語言直接相關的目錄及其功能以下bash

  • Include:C頭文件,與部署好的頭文件目錄~/.pyenv/versions/3.6.6/include/python3.6m/中的文件一致(嚴格來講,部署好的頭文件目錄中會多一個自動生成的pyconfig.h文件),這些頭文件定義了Python語言的底層抽象結構。
  • Lib:Python語言庫,這部分不參與Python的編譯,而是用Python語言寫好的模塊庫。
  • Modules:用C語言實現的Python內置庫。
  • Objects:Python內置對象的C語言實現以及抽象接口的實現。
  • Parser:Python編譯器的前端,詞法分析器語法分析器。後者就是基於龍書的LL(1)實現的。
  • Programs:可執行文件~/.pyenv/versions/3.6.6/bin/python的源碼所在的目錄。
  • Python:Python虛擬機所在的目錄,也是整個Python語言較爲核心的部分。

使用下面的圖示能更好的展現這些目錄以前的相互關係,虛線箭頭表示提供接口定義,實線箭頭表示提供服務,自頂向下的結構也體現了語言設計在架構上的層次關係。數據結構

clipboard.png

Include目錄

從上面這些模塊的大體功能上分析,咱們能夠判斷出IncludeObjectsPython中的代碼比較重要。咱們先看一下這三個目錄包含的代碼量架構

$ cat Include/* Objects/* Python/* | wc -l
cat: Objects/clinic: Is a directory
cat: Objects/stringlib: Is a directory
cat: Python/clinic: Is a directory
  215478

21萬行代碼的閱讀量有點略大,咱們仍是先挨個看看這些目錄中文件的命名、大小以及一些註釋,看能不能獲得一些線索。

$ wc -l Include/*.h | sort -k1
     ...
     324 pystate.h
     370 objimpl.h
     499 dynamic_annotations.h
     503 pyerrors.h
     637 Python-ast.h
     767 pyport.h
    1077 object.h
    1377 abstract.h
    2342 unicodeobject.h
   15980 total

從文件名和文件大小能夠初步判斷object.habstract.h是兩個比較重要的頭文件,實際上它們定義了Python底層的抽象對象以及統一的抽象接口
unicodeobject.h雖然體積大,可是有不少跟它相似的頭文件,如boolobject.hlongobject.hfloatobject.h等等,這些頭文件應該是內置類型的頭文件,咱們能夠暫時不去理會這些文件,對語言的整體理解不會形成困難。

爲了避免漏掉一些重要的頭文件,咱們快速閱讀一下其餘頭文件中可能包含的一些引導性的註釋,發現這些頭文件也比較重要:

  • Python.h:元頭文件,一般在寫Python的C擴展時會包含它。
  • ceval.h:做爲Python/ceval.c的頭文件,而Python/ceval.c負責運行編譯後的代碼。
  • code.h:包含字節碼相關的底層抽象。
  • compile.h抽象語法樹的編譯接口。
  • objimpl.h:跟內存相關的抽象對象高層接口,如內存分配,初始化,垃圾回收等等。
  • pystate.h線程狀態解釋器狀態以及它們的接口。
  • pythonrun.h:Python代碼的語法分析與執行接口。

經過以上篩選,咱們看看還剩下多少代碼:

$ cat object.h abstract.h objimpl.h Python.h ceval.h code.h compile.h pystate.h pythonrun.h | wc -l
    3950

核心頭文件壓縮到不到4千行。

Objects目錄

用相似的思路,咱們能從Objects目錄中篩選出一些比較重要的文件

  • abstract.c抽象對象的接口實現。
  • codeobject.c:字節碼對象的實現。
  • object.c:通用對象操做的實現。
  • obmalloc.c:內存分配相關實現。
  • typeobject.cType對象實現。

統計一下代碼量

$ wc -l abstract.c codeobject.c object.c obmalloc.c typeobject.c
    3246 abstract.c
     921 codeobject.c
    2048 object.c
    2376 obmalloc.c
    7612 typeobject.c
   16203 total

一會兒新增了1.6萬行,畢竟是實打實的C語言實現。

另外還有一些具象化的對象實現文件,雖然它們跟longobject.cdictobject.c之類的對象實現相似,都是具體的對象,可是它們跟Python語言特性比較相關,在這裏也把它們列出來,作爲備份。

  • classobject.c:類對象實現。
  • codeobject.c:代碼對象實現。
  • frameobject.c:Frame對象實現。
  • funcobject.c:函數對象實現。
  • methodobject.c:方法對象實現。
  • moduleobject.c:模塊對象實現。

順便統計下行數

$ wc -l classobject.c codeobject.c frameobject.c funcobject.c methodobject.c moduleobject.c
     648 classobject.c
     921 codeobject.c
    1038 frameobject.c
    1031 funcobject.c
     553 methodobject.c
     802 moduleobject.c
    4993 total

Objects目錄中合計約2.1萬行。經過探索這些源代碼,咱們看出Python的一個設計原則就是:一切皆對象。

嚴格來講,只有Python語言暴露給外部使用的部分才抽象成了對象,而一些僅在內部使用的數據結構則沒有對象封裝,如後面會提到的 解釋器狀態線程狀態等。

Python目錄

依然通過一輪篩選,能獲得下面這些比較重要的文件

  • ast.c:將具體語法樹轉換成抽象語法樹,主要函數是PyAST_FromNode()
  • ceval.c:執行編譯後的字節碼。
  • ceval_gil.h全局解釋器鎖(Global Interpreter Lock,GIL)的接口。
  • compile.c:將抽象語法樹編譯成Python字節碼。
  • pylifecycle.c:Python解釋器的頂層代碼,包括解釋器的初始化以及退出。
  • pystate.c線程狀態解釋器狀態,以及它們的接口實現。
  • pythonrun.c:Python解釋器的頂層代碼,包括解釋器的初始化以及退出。

可以注意到,pylifecycle.cpythonrun.c的功能是相似的,實際上查閱Python開發歷史記錄能發現前者是由於開發須要從後者分離出來的。統計一下代碼的數量:

$ wc -l ast.c ceval.c ceval_gil.h compile.c pystate.c pythonrun.c
    5277 ast.c
    5600 ceval.c
     270 ceval_gil.h
    5329 compile.c
     958 pystate.c
    1596 pythonrun.c
   19030 total

這樣濃縮下來IncludeObjectsPython三個文件夾中比較重要的代碼一共大約4.4萬行,先不說咱們這樣篩選出來的一波有沒有漏掉重要信息,其餘不少支持性的代碼都尚未包含進去。至少目前有了一個大的輪廓,接下來在深刻代碼的時候能夠慢慢擴展開。

頂層調用樹

前面討論了Python源碼的主要目錄結構,以及其中主要的源文件。這裏咱們換一個思路,看看一個Python源文件是如何在Python解釋器裏面運行的。調用Python的可執行文件~/.pyenv/versions/3.6.6/bin/python和調用咱們編寫的其餘C語言程序在方式上並無太大區別,不一樣之處在於Python可執行文件讀取的Python源文件,並執行其中的代碼。Python之於C就如同C之於彙編,只是Python編譯的字節碼在Python虛擬機上運行,彙編代碼直接在物理機上運行(嚴格來講還須要轉換成機器代碼)。

如下面這條Python源文件運行爲例來考察Python可執行文件的執行過程(你們能夠玩玩這個生命遊戲,運氣好能看到滑翔機)。

$ python ~/.pyenv/sources/3.6.6/Python-3.6.6/Tools/demo/life.py

既然Python的可執行文件是C語言編譯成的,那麼必定有C語言的入口函數main,它就位於Python源碼的./Programs/python.c文件中。

int
main(int argc, char **argv)
{
    // ...
    res = Py_Main(argc, argv_copy);
    // ...
}

順藤摸瓜,咱們能夠梳理出調用樹的主幹部分。下面的樹形結構中,冒號左邊爲函數名,右邊表示函數定義所在的C源文件,樹形結構表示函數定義中包含的其餘函數嵌套調用。

main: Programs/python.c
└─ Py_Main: Modules/main.c
   ├─ Py_Initialize: Python/pylifecycle.c
   │  ├─ PyInterpreterState_New: Python/pystate.c
   │  ├─ PyThreadState_New: Python/pystate.c
   │  ├─ _PyGILState_Init: Python/pystate.c
   │  └─ _Py_ReadyTypes: Objects/object.c
   ├─ run_file: Modules/main.c
   │  └─ PyRun_FileExFlags: Python/pythonrun.c
   │     ├─ PyParser_ASTFromFileObject: Python/pythonrun.c
   │     │  ├─ PyParser_ParseFileObject: Parser/parsetok.c
   │     │  └─ PyAST_FromNodeObject: Python/ast.c
   │     └─ run_mod: Python/pythonrun.c
   │        ├─ PyAST_CompileObject: Python/compile.c
   │        └─ PyEval_EvalCode: Python/ceval.c
   │           ├─ PyFrame_New: Objects/frameobject.c
   │           └─ PyEval_EvalFrameEx: Python/ceval.c
   └─ Py_FinalizeEx: Python/pylifecycle.c

不得不說,Python源碼的可讀性很是好,這些函數的命名方式都是自解釋的。Python源文件的運行大體分爲兩個步驟:

  1. Py_Initialize:初始化過程,主要涉及到解釋器狀態線程狀態全局解釋器鎖以及內置類型的初始化。
  2. run_file:運行源文件,能夠分爲三個小步驟

    1. PyParser_ASTFromFileObject:對源文件的文本進行語法分析,獲得抽象語法樹
    2. PyAST_CompileObject:將抽象語法樹編譯成PyCodeObject對象。
    3. PyEval_EvalCode:在Python虛擬機中運行PyCodeObject對象。
  3. Py_FinalizeEx:源文件執行結束後的清理工做。

用流程圖的形式表示上述調用樹的主幹部分應該更加清晰明瞭。

clipboard.png

須要指出的是,解釋器循環真正執行的是PyEval_EvalFrameEx函數,它的參數是PyFrameObject對象,該對象爲PyCodeObject對象提供了執行的上下文環境,因此PyFrameObjectPyCodeObject都是很是核心的對象。Python提供了一些工具讓咱們能夠查看編譯後的代碼對象,即對編譯好的函數進行反彙編。下面的例子雖然簡單,但已經能給人清晰的直觀認識

>>> from dis import dis
>>> class C(object):
...     def __init__(self, x):
...         self.x = x
...     def add(self, y):
...         return self.x + y
...
>>> dis(C)
Disassembly of __init__:
  3           0 LOAD_FAST                1 (x)
              2 LOAD_FAST                0 (self)
              4 STORE_ATTR               0 (x)
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE

Disassembly of add:
  5           0 LOAD_FAST                0 (self)
              2 LOAD_ATTR                0 (x)
              4 LOAD_FAST                1 (y)
              6 BINARY_ADD
              8 RETURN_VALUE

反編譯的結果是一系列的操做碼。頭文件Include/opcode.h包含了Python虛擬機的全部操做碼。能看出上面simple_tuplesimple_list這兩個函數反編譯後的最大區別麼?tuple是做爲常量被加載進來的,而list的生成還須要調用BUILD_LIST。緣由在於tuple在Python的運行時會進行緩存,也就是每次使用無需請求操做系統內核以得到內存空間。對比一下使用tuplelist的耗時狀況

>>> %timeit x = (1, 2, 3)
10.9 ns ± 0.0617 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each)
>>> %timeit x = [1, 2, 3]
46.5 ns ± 0.186 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

從統計結果能看出,tuple的在效率上的優點很是明顯。若是某一段調用特別頻繁的代碼中有些list能夠替換成tuple,千萬不要猶豫。

總結

咱們能夠試着爲文章開頭第一個問題提供一些思路。咱們知道,對計算機作任何形式上的抽象都有可能傷害到計算的效率,對於Python來講有如下幾點

  1. Python對象的內存部署方式是以在知足必定效率的前提下足夠通用爲目標的,所以在面臨特定問題時它不必定是最優的。
  2. Python是動態類型語言,並非編譯型語言,致使代碼在運行時是可變的,從Python將抽象語法樹PyCodeObject對象暴露出來這一點就能看出。
  3. 全局解釋器鎖也會妨礙使用多進程來實現性能的提高。
  4. Python虛擬機做爲對CPU硬件的抽象也是無法甩鍋的。

因此爲了提升Python程序的效率,咱們須要深刻了解Python對象的實現原理、PyCodeObject的特性以及全局解釋器和Python虛擬機的限制。之於文章開頭的其餘問題,咱們將隨着Python源碼的深刻研究慢慢展開。

如今咱們對Python代碼的運行有了一個宏觀的理解,並且大量的細節都有待深刻研究。經過對調用樹主幹部分的梳理,能看出其餘比較重要的支持性模塊還包括Python抽象對象PyObject抽象語法樹及其編譯,PyCodeObject對象,PyFrameObject對象,解釋器狀態線程狀態全局解釋器鎖。在之後的文章中,咱們會分別對這些模塊進行探討。

相關文章
相關標籤/搜索