Python歷史「解密」Python底層邏輯 及Python 字節碼介紹(轉帖)

 

帖子來源:https://www.ituring.com.cn/article/507878

一次純粹的hacking

Python的做者,Guido von Rossum,荷蘭人。1982年,Guido從阿姆斯特丹大學得到了數學和計算機碩士學位。儘管,他算得上是一位數學家,但他更加享受計算機帶來的樂趣,熱衷於作任何和編程相關的活兒。html

80年代,掀起了我的電腦浪潮,但受限於我的電腦配置低,全部的編譯器的核心是作優化,以便讓程序可以運行。在那個時代,程序員巴不得用手榨取計算機每一寸的能力。有人甚至認爲C語言的指針是在浪費內存,至於動態類型,內存自動管理,面向對象…… 別想了,那會讓你的電腦陷入癱瘓。python

而這種編程方式讓Guido感到苦惱。Guido知道如何用C語言寫出一個功能,但整個編寫過程須要耗費大量的時間。linux

不過,他還有另外一個選擇shell。shell能夠像膠水同樣,將UNIX下的許多功能鏈接在一塊兒。UNIX的管理員們經常用shell去寫一些簡單的腳本,以進行一些系統維護的工做,好比按期備份、文件系統管理等等。然而,shell的本質是調用命令,並不能全面的調動計算機的功能。git

Guido但願有一種語言,這種語言可以像C語言那樣,可以全面調用計算機的功能接口,又能夠像shell那樣輕鬆的編程。程序員

**ABC語言讓Guido看到但願。**ABC是由荷蘭的數學和計算機研究所開發的,Guido在該研究所工做,並參與到ABC語言的開發。ABC 語言是一個致力於爲初學者設計編程環境的長達 10 年的研究項目,與當時的大部分語言不一樣,ABC語言的目標是「讓用戶感受更好」。github

好比下面是一段來自Wikipedia的ABC程序,這個程序用於統計文本中出現的詞的總數: HOW TO RETURN words document: PUT {} IN collection FOR line IN document: FOR word IN split line: IF word not.in collection: INSERT word IN collection RETURN collectionshell

HOW TO用於定義一個函數。一個Python程序員應該很容易理解這段程序。ABC語言使用冒號和縮進來表示程序塊。行 尾沒有分號。for和if結構中也沒有括號() 。賦值採用的是PUT,而不是更常見的等號。這些改動讓ABC程序讀起來像一段文字。數據庫

儘管ABC已經具有了良好的可讀性和易用性,但最終卻也沒能流行起來。緣由在於:編程

  • 硬件上的困難:ABC語言編譯器須要比較高配置的電腦才能運行,而當時電腦使用者,更多考慮程序效率,而非語言難度;
  • 一個語言設計的致命問題:其可拓展性較差,若是想在ABC語言中增長功能,好比對圖形化的支持,就必須改動不少地方。
  • 不能直接進行IO:ABC語言不能直接操做文件系統。儘管你能夠經過諸如文本流的方式導入數據,但ABC沒法直接讀寫文件。

輸入輸出的困難對於計算機語言來講是致命的。你能想像一個打不開車門的跑車麼?數據結構

ABC的前車可鑑,給Guido帶來啓示。

1989年,爲了打發聖誕節假期,Guido開始寫Python語言的編譯器。Python這個名字,來自Guido所摯愛的電視劇Monty Python's Flying Circus。他但願這個新的叫作Python的語言,能符合他的理想創造一種C和shell之間,功能全面,易學易用,可拓展的語言。Guido做爲一個語言設計愛好者,已經有過設計語言的嘗試。這一次,也不過是一次純粹的hacking行爲。

Python解釋器的誕生

1991 年,第一個 Python 解釋器誕生,它是用 C 語言實現的,並可以調用 C 語言的庫文件。從一出生,Python已經具備了:類,函數,異常處理,包含表和詞典在內的核心數據類型,以及模塊爲基礎的拓展系統。

這裏須要牽扯一個「編譯器」的概念,其主要做用是便於人編寫,閱讀,維護的高級計算機語言翻譯爲計算機能識別,運行的低級機器語言的程序。

編譯器翻譯語言方式有2種:編譯、解釋。 enter image description here

①編譯型語言:需經過編譯器(compiler)將源代碼編譯成機器碼,以後才能執行的語言。

通常需通過編譯(compile)、連接(linker)這兩個步驟。編譯是把源代碼編譯成機器碼,連接是把各個模塊的機器碼和依賴庫串連起來生成可執行文件。

②解釋型語言:解釋性語言的程序不須要編譯,相比編譯型語言省了道工序,解釋性語言在運行程序的時候才逐行翻譯。

Python是一種解釋型語言,它的源代碼不須要編譯,能夠直接從源代碼運行程序。Python解釋器將源代碼轉換爲字節碼,而後把編譯好的字節碼轉發到Python虛擬機(Python Virtual Machine,PVM)中執行。 enter image description here

當咱們執行Python代碼的時候,在Python解釋器用四個過程「拆解」咱們的代碼:

  • 首先,當你把鍵入代碼交給Python處理的時候會先進行詞法分析,若是你鍵入關鍵字或者當輸入關鍵字有誤時,都會被詞法分析所觸發,不正確的代碼將不會被執行。
  • Python會進行語法分析,例如當"for i in test:"中,test後面的冒號若是被寫爲其餘符號,代碼依舊不會被執行。
  • 進入最關鍵的過程,在執行Python前,Python會生成.pyc文件,這個文件就是字節碼。
  • 將編譯好的字節碼轉發Python虛擬機中進行執行:由Python Virtual Machine(Python虛擬機)來執行這些編譯好的字節碼。

什麼是字節碼(bytecode)?

簡單的說它就是一個從源代碼編譯而來的中間文件(用於不一樣操做系統平臺的解釋器執行)。好比,a說日語,b說中文,溝通起來不順暢通,請一個翻譯,把a和b的語言都翻譯成英語,這個英語就能夠理解成bytecode, 一種中間語言。

bytecode的好處就是 加載快,並且能夠跨平臺, 一樣一份bytecode,只要有操做系統平臺上有相應的Python解釋器,就能夠執行,而不須要源代碼。不一樣版本的Python編譯的字節碼是不兼容的,Python 2.6編譯的bytecode拿到Python 2.7上去執行就不行了。

如何生成字節碼?

Python解釋器通常會自動把.py文件轉換成bytecode,而後再執行它。當你第一次把.py文件看成module導入,或者對應的.py文件比.pyc文件的修改時間還要新時,Python解釋器都會再從source code生成相應的新bytecode。這樣當你下次再次運行程序時,就會直接從bytecode運行,從而節省便宜時間。

Ps:這裏須要注意,有些狀況bytecode並不會生成:

  • 遇到目錄寫權限的問題時。(好比你編寫代碼和運行代碼使用的具備不一樣權限的用戶角色,Linux上很常見)
  • 運行一個script並不會被當成是import操做,因此可能也不會生成bytecode。(好比:你有個一個a.py的文件,其中在a.py裏,你import了b.py,那麼運行python a.py後,會生成b.pyc,而不會生成a.pyc)

☞拓展閱讀:

(下文詳細說明Python的工做機制和Python虛擬機內幕)

Python 字節碼介紹

.pyc文件是什麼?

Python源碼編譯的結果就是 PyCodeObject ,每一個做用域會編譯出一個對應的代碼對象,其中名爲co_code的PyStringObject保存着代碼對象的字節碼。

一個Python源文件就是一個模塊。每一個模塊頂層的代碼對象經過marshal序列化以後就獲得了.pyc文件。marshal以little-endian字節序來序列化數據。

那嵌套於頂層做用域裏面的那些做用域,例如函數、類的定義,它們對應的代碼對象在哪裏?它們每個都乖乖的躺在上一層做用域的代碼對象的co_const(常量池)域裏,因此其實頂層代碼對象已經嵌套包含了底下其它做用域的代碼對象。

☞拓展閱讀:

(下文主要結合實例說明了.pyc文件結構)

Python 2.6.2的.pyc文件格式

如何對.pyc文件文件進行反編譯?

python文件若是要發佈的話,有時候仍是不免想保護一下本身的源碼,有些人就直接編譯成了pyc文件,由於這樣既能夠保留跨平臺的特性,又能夠不能直接看到代碼,也看到網上不少人說爲了保護本身的代碼能夠編譯成pyc文件。

用pyc文件能夠保護python代碼的想法實際上是不正確的 ,pyc文件是能夠很容易被反編譯的,好比說比較著名的uncompyle6 庫(https://github.com/rocky/python-uncompyle6),用來反編譯文件最爽不過了,幾乎支持python全版本的pyc文件的反編譯。

爲何要作代碼分析?

通常來講,代碼分析重要性的判斷比較主觀,不一樣的人有不一樣的認識。Python是用C來實現的,因此對於Python的性能或代碼質量的評估能夠經過 dis模塊 獲取到對應的字節碼指令來進行評估。

通常來講一個Python語句會對應若干字節碼指令,Python的字節碼是一種相似彙編指令的中間語言,可是一個字節碼指令並非對應一個機器指令(二進制指令),而是對應一段C代碼,而不一樣的指令的性能不一樣,因此不能單獨經過指令數量來判斷代碼的性能,而是要經過 查看調用比較頻繁的指令的代碼 來確認一段程序的性能。

一個Python的程序會有若干代碼塊組成,例如一個Python文件會是一個代碼塊,一個類,一個函數都是一個代碼塊,一個代碼塊會對應一個運行的上下文環境以及一系列的字節碼指令。

dis模塊主要是用來分析字節碼的一個內置模塊。dis 模塊的文檔 可讓你遍歷它的內容,而且提供一個字節碼指令可以作什麼和有什麼樣的參數的完整清單。

☞拓展閱讀:

(下文主要說明了dis模塊的使用)

Python反編譯之字節碼

Python開發者如何寫出高質量的代碼?

要不這樣吧,若是編程語言裏有個地方你弄不明白,而正好又有我的用了這個功能,那就開槍把他打死。這比學習新特性要容易些,而後過不了多久,那些活下來的程序員就會開始用 0.9.6 版的 Python,並且他們只須要使用這個版本中易於理解的那一小部分就行了(眨眼)。

—— Tim Peters

傳奇的核心開發者,「Python 之禪」做者

給 comp.lang.python Usenet 小組的留言,2002 年 12 月 23 日,「Acrimony in c.l.p」。

Python 官方教程的開頭是這樣寫的:「Python 是一門既容易上手又強大的編程語言。」這句話自己並沒有大礙,但須要注意的是,正由於它既好學又好用,因此不少 Python 程序員只用到了其強大功能的一小部分。

只須要幾個小時,經驗豐富的程序員就能學會用 Python 寫出實用的程序。然而隨着這最初高產的幾個小時變成數週甚至數月,在那些先入爲主的編程語言的影響下,開發者們會慢慢地寫出帶着「口音」的 Python 代碼。與此同時,你會發現,本身在持續陷入基本的熟練程度,卻無從提高本身的編程技能。

其實,掌握Python編程不只要掌握該語言的理論方面, 理解和採用社區使用的慣例和最佳實踐也一樣重要。 並且這些技巧能夠很好的幫助你避免重複勞動,寫出簡潔、流暢、易讀、易維護的代碼。

☞拓展資料:

《流暢的Python》

  • PSF研究員、知名PyCon演講者心血之做
  • Python核心開發人員擔綱技術審校
  • 全面深刻,對Python語言關鍵特性剖析到位
  • 大量詳盡代碼示例,並附有主題相關高質量參考文獻和視頻連接
  • 兼顧Python 3和Python 2

本書致力於幫助Python開發人員挖掘這門語言及相關程序庫的優秀特性,寫出簡潔、流暢、易讀、易維護的代碼。特別是深刻探討了針對數據庫處理時生成器的具體應用、特性描述符(ORM的關鍵),以及Python式的對象:協議與接口、抽象基類及多重繼承。

《深刻理解Python特性》

  • 上市兩個月獲 Amazon 百餘條五星評價
  • 影響全球1 000 000以上程序員的PythonistaCafe社區創始人Dan Bader手把手帶你提高Python實踐技能
  • 與《流暢的Python》互爲補充,Python進階必備

本書致力於幫助Python開發人員挖掘這門語言及相關程序庫的優秀特性,避免重複勞動,同時寫出簡潔、流暢、易讀、易維護的代碼。用好Python須要瞭解的最重要的特性、Python 2過渡到Python 3須要掌握的現代模式、有其餘編程語言背景想快速上手Python的程序員須要特別注意的問題,等等,本書均可以解決。

參考資料:

https://blog.csdn.net/miaodalengshui/article/details/77451262

https://mp.weixin.qq.com/s/qqHQYyqFsCYVIYjmWOF4jQ

https://linux.cn/article-9816-1.html

https://blog.csdn.net/helloxiaozhe/article/details/78104975

https://www.cnblogs.com/mlgjb/p/7899534.html

 

 

原文連接:https://linux.cn/article-9816-1.html

Python 字節碼介紹

 

 

若是你曾經編寫過 Python,或者只是使用過 Python,你或許常常會看到 Python 源代碼文件——它們的名字以 .py 結尾。你可能還看到過其它類型的文件,好比以 .pyc 結尾的文件,或許你可能據說過它們就是 Python 的 「字節碼bytecode」 文件。(在 Python 3 上這些可能不容易看到 —— 由於它們與你的 .py 文件不在同一個目錄下,它們在一個叫 __pycache__ 的子目錄中)或者你也據說過,這是節省時間的一種方法,它能夠避免每次運行 Python 時去從新解析源代碼。

可是,除了 「噢,原來這就是 Python 字節碼」 以外,你還知道這些文件能作什麼嗎?以及 Python 是如何使用它們的?

若是你不知道,那你走運了!今天我將帶你瞭解 Python 的字節碼是什麼,Python 如何使用它去運行你的代碼,以及知道它是如何幫助你的。

Python 如何工做

Python 常常被介紹爲它是一個解釋型語言 —— 其中一個緣由是在程序運行時,你的源代碼被轉換成 CPU 的原生指令 —— 但這樣的見解只是部分正確。Python 與大多數解釋型語言同樣,確實是將源代碼編譯爲一組虛擬機指令,而且 Python 解釋器是針對相應的虛擬機實現的。這種中間格式被稱爲 「字節碼」。

所以,這些 .pyc 文件是 Python 悄悄留下的,是爲了讓它們運行的 「更快」,或者是針對你的源代碼的 「優化」 版本;它們是你的程序在 Python 虛擬機上運行的字節碼指令。

咱們來看一個示例。這裏是用 Python 寫的經典程序 「Hello, World!」:

  1. def hello()
  2.     print("Hello, World!")

下面是轉換後的字節碼(轉換爲人類可讀的格式):

  1. 2 0 LOAD_GLOBAL 0 (print)
  2. 2 LOAD_CONST 1 ('Hello, World!')
  3. 4 CALL_FUNCTION 1

若是你輸入那個 hello() 函數,而後使用 CPython 解釋器去運行它,那麼上述列出的內容就是 Python 所運行的。它看起來可能有點奇怪,所以,咱們來深刻了解一下它都作了些什麼。

Python 虛擬機內幕

CPython 使用一個基於棧的虛擬機。也就是說,它徹底面向棧數據結構的(你能夠 「推入」 一個東西到棧 「頂」,或者,從棧 「頂」 上 「彈出」 一個東西來)。

CPython 使用三種類型的棧:

  1. 調用棧call stack。這是運行 Python 程序的主要結構。它爲每一個當前活動的函數調用使用了一個東西 —— 「幀frame」,棧底是程序的入口點。每一個函數調用推送一個新的幀到調用棧,每當函數調用返回後,這個幀被銷燬。
  2. 在每一個幀中,有一個計算棧evaluation stack (也稱爲數據棧data stack)。這個棧就是 Python 函數運行的地方,運行的 Python 代碼大多數是由推入到這個棧中的東西組成的,操做它們,而後在返回後銷燬它們。
  3. 在每一個幀中,還有一個塊棧block stack。它被 Python 用於去跟蹤某些類型的控制結構:循環、try / except 塊、以及 with 塊,所有推入到塊棧中,當你退出這些控制結構時,塊棧被銷燬。這將幫助 Python 瞭解任意給定時刻哪一個塊是活動的,好比,一個 continue 或者 break 語句可能影響正確的塊。

大多數 Python 字節碼指令操做的是當前調用棧幀的計算棧,雖然,還有一些指令能夠作其它的事情(好比跳轉到指定指令,或者操做塊棧)。

爲了更好地理解,假設咱們有一些調用函數的代碼,好比這個:my_function(my_variable, 2)。Python 將轉換爲一系列字節碼指令:

  1. 一個 LOAD_NAME 指令去查找函數對象 my_function,而後將它推入到計算棧的頂部
  2. 另外一個 LOAD_NAME 指令去查找變量 my_variable,而後將它推入到計算棧的頂部
  3. 一個 LOAD_CONST 指令去推入一個實整數值 2 到計算棧的頂部
  4. 一個 CALL_FUNCTION 指令

這個 CALL_FUNCTION 指令將有 2 個參數,它表示那個 Python 須要從棧頂彈出兩個位置參數;而後函數將在它上面進行調用,而且它也同時被彈出(對於函數涉及的關鍵字參數,它使用另外一個不一樣的指令 —— CALL_FUNCTION_KW,但使用的操做原則相似,以及第三個指令 —— CALL_FUNCTION_EX,它適用於函數調用涉及到參數使用 *** 操做符的狀況)。一旦 Python 擁有了這些以後,它將在調用棧上分配一個新幀,填充到函數調用的本地變量上,而後,運行那個幀內的 my_function 字節碼。運行完成後,這個幀將被調用棧銷燬,而在最初的幀內,my_function 的返回值將被推入到計算棧的頂部。

訪問和理解 Python 字節碼

若是你想玩轉字節碼,那麼,Python 標準庫中的 dis 模塊將對你有很是大的幫助;dis 模塊爲 Python 字節碼提供了一個 「反彙編」,它可讓你更容易地獲得一我的類可讀的版本,以及查找各類字節碼指令。dis 模塊的文檔 可讓你遍歷它的內容,而且提供一個字節碼指令可以作什麼和有什麼樣的參數的完整清單。

例如,獲取上面的 hello() 函數的列表,能夠在一個 Python 解析器中輸入以下內容,而後運行它:

  1. import dis
  2. dis.dis(hello)

函數 dis.dis() 將反彙編一個函數、方法、類、模塊、編譯過的 Python 代碼對象、或者字符串包含的源代碼,以及顯示出一我的類可讀的版本。dis 模塊中另外一個方便的功能是 distb()。你能夠給它傳遞一個 Python 追溯對象,或者在發生預期外狀況時調用它,而後它將在發生預期外狀況時反彙編調用棧上最頂端的函數,並顯示它的字節碼,以及插入一個指向到引起意外狀況的指令的指針。

它也能夠用於查看 Python 爲每一個函數構建的編譯後的代碼對象,由於運行一個函數將會用到這些代碼對象的屬性。這裏有一個查看 hello() 函數的示例:

  1. >>> hello.__code__
  2. <code object hello at 0x104e46930, file "<stdin>", line 1>
  3. >>> hello.__code__.co_consts
  4. (None, 'Hello, World!')
  5. >>> hello.__code__.co_varnames
  6. ()
  7. >>> hello.__code__.co_names
  8. ('print',)

代碼對象在函數中能夠以屬性 __code__ 來訪問,而且攜帶了一些重要的屬性:

  • co_consts 是存在於函數體內的任意實數的元組
  • co_varnames 是函數體內使用的包含任意本地變量名字的元組
  • co_names 是在函數體內引用的任意非本地名字的元組

許多字節碼指令 —— 尤爲是那些推入到棧中的加載值,或者在變量和屬性中的存儲值 —— 在這些元組中的索引做爲它們參數。

所以,如今咱們可以理解 hello() 函數中所列出的字節碼:

  1. LOAD_GLOBAL 0:告訴 Python 經過 co_names (它是 print 函數)的索引 0 上的名字去查找它指向的全局對象,而後將它推入到計算棧
  2. LOAD_CONST 1:帶入 co_consts 在索引 1 上的字面值,並將它推入(索引 0 上的字面值是 None,它表示在 co_consts 中,由於 Python 函數調用有一個隱式的返回值 None,若是沒有顯式的返回表達式,就返回這個隱式的值 )。
  3. CALL_FUNCTION 1:告訴 Python 去調用一個函數;它須要從棧中彈出一個位置參數,而後,新的棧頂將被函數調用。

「原始的」 字節碼 —— 是非人類可讀格式的字節 —— 也能夠在代碼對象上做爲 co_code 屬性可用。若是你有興趣嘗試手工反彙編一個函數時,你能夠從它們的十進制字節值中,使用列出 dis.opname 的方式去查看字節碼指令的名字。

字節碼的用處

如今,你已經瞭解的足夠多了,你可能會想 「OK,我認爲它很酷,可是知道這些有什麼實際價值呢?」因爲對它很好奇,咱們去了解它,可是除了好奇以外,Python 字節碼在幾個方面仍是很是有用的。

首先,理解 Python 的運行模型能夠幫你更好地理解你的代碼。人們都開玩笑說,C 是一種 「可移植彙編器」,你能夠很好地猜想出一段 C 代碼轉換成什麼樣的機器指令。理解 Python 字節碼以後,你在使用 Python 時也具有一樣的能力 —— 若是你能預料到你的 Python 源代碼將被轉換成什麼樣的字節碼,那麼你能夠知道如何更好地寫和優化 Python 源代碼。

第二,理解字節碼能夠幫你更好地回答有關 Python 的問題。好比,我常常看到一些 Python 新手困惑爲何某些結構比其它結構運行的更快(好比,爲何 {}dict() 快)。知道如何去訪問和閱讀 Python 字節碼將讓你很容易回答這樣的問題(嘗試對比一下: dis.dis("{}")dis.dis("dict()") 就會明白)。

最後,理解字節碼和 Python 如何運行它,爲 Python 程序員不常用的一種特定的編程方式提供了有用的視角:面向棧的編程。若是你之前歷來沒有使用過像 FORTH 或 Fator 這樣的面向棧的編程語言,它們可能有些古老,可是,若是你不熟悉這種方法,學習有關 Python 字節碼的知識,以及理解面向棧的編程模型是如何工做的,將有助你開拓你的編程視野。

相關文章
相關標籤/搜索