11. 深刻Python虛擬機,探索虛擬機執行字節碼的奧祕

楔子

這一次咱們就來剖析Python運行字節碼的原理,咱們知道Python虛擬機是Python的核心,在源代碼被編譯成PyCodeObject對象時,就將由Python虛擬機接手整個工做。Python虛擬機會從PyCodeObject中讀取字節碼,並在當前的上下文中執行,直到全部的字節碼都被執行完畢。python

Python虛擬機的執行環境

Python的虛擬機其實是在模擬操做系統運行可執行文件的過程,咱們先來看看在一臺普通的x86的機器上,可執行文件是以什麼方式運行的。在這裏主要關注運行時棧的棧幀,如圖所示:數組

x86體系處理器經過棧維護調用關係,每次函數調用時就在棧上分配一個幀用於保存調用上下文以及臨時存儲。CPU中有兩個關鍵寄存器,rsp指向當前棧頂,rbp指向固然棧幀。每次調用函數時,調用者(Caller)負責準備參數、保存返回地址,並跳轉到被調用函數中執行代碼;做爲被調用者(Callee),函數先將當前rbp寄存器壓入棧,並將rbp設爲當前棧頂(保存當前新棧幀的位置)。由此,rbp寄存器與每一個棧幀中保存調用者棧幀地址一塊兒完美地維護了函數調用關係鏈。數據結構

咱們以Python中的代碼爲例:閉包

def f(a, b):
    return a + b

def g():
    return f()

g()

當程序進入到函數 f 中執行時,那麼顯然調用者的幀就是函數 g 的棧幀,而當前幀則是 f 的棧幀。架構

解釋一下:棧是先入後出的數據結構,從棧頂到棧底地址是增大的。對於一個函數而言,其全部對局部變量的操做都在本身的棧幀中完成,而調用函數的時候則會爲調用的函數建立新的棧幀。框架

在上圖中,咱們看到運行時棧的地址是從高地址向低地址延伸的。當在函數 g 中調用函數 f 的時候,系統就會在地址空間中,於 g 的棧幀以後建立 f 的棧幀。固然在函數調用的時候,系統會保存上一個棧幀的棧指針(rsp)和幀指針(rbp)。當函數的調用完成時,系統就又會把rsp和rbp的值恢復爲建立 f 棧幀以前的值,這樣程序的流程就又回到了 g 函數中,固然程序的運行空間則也又回到了函數g的棧幀中,這就是可執行文件在x86機器上的運行原理。函數

而上一章咱們說Python源代碼通過編譯以後,全部字節碼指令以及其餘靜態信息都存儲在PyCodeObject當中,那麼是否是意味着Python虛擬機就在PyCodeObject對象上進行全部的動做呢?其實不能給出惟一的答案,由於儘管PyCodeObject包含了關鍵的字節碼指令以及靜態信息,可是有一個東西,是沒有包含、也不可能包含的,就是程序運行的動態信息--執行環境。oop

var = "satori"


def f():
    var = 666
    print(var)

f()
print(var)

首先代碼當中出現了兩個print(var),它們的字節碼指令是相同的,可是執行的效果卻顯然是不一樣的,這樣的結果正是執行環境的不一樣所產生的。由於環境的不一樣,var的值也是不一樣的。所以同一個符號在不一樣環境中對應不一樣的類型、不一樣的值,必須在運行時進行動態地捕捉和維護,這些信息是不可能在PyCodeObject對象中被靜態的存儲的。學習

因此咱們還須要執行環境,這裏的執行環境和咱們下面將要說的名字空間比較相似(名字空間暫時就簡單地理解爲做用域便可)。可是名字空間僅僅是執行環境的一部分,除了名字空間,在執行環境中,還包含了其餘的一些信息。優化

所以對於上面代碼,咱們能夠大體描述一下流程:

  • 當python在執行第一條語句時,已經建立了一個執行環境,假設叫作A
  • 全部的字節碼都會在這個環境中執行,Python能夠從這個環境中獲取變量的值,也能夠修改。
  • 當發生函數調用的時候,Python會在執行環境A中調用函數f的字節碼指令,會在執行環境A以外從新建立一個執行環境B
  • 在環境B中也有一個名字爲var的對象,可是因爲環境的不一樣,var也不一樣。兩我的都叫小明,但一個是北京的、一個是上海的,因此這二者沒什麼關係
  • 一旦當函數f的字節碼指令執行完畢,會將當前f的棧幀銷燬(也能夠保留下來),再回到調用者的棧幀中來。就像是遞歸同樣,每當調用函數就會建立一個棧幀,一層一層建立,一層一層返回。

因此Python在運行時的時候,並非在PyCodeObject對象上執行操做的,而是咱們一直在說的棧幀對象(PyFrameObject),從名字也能看出來,這個棧幀也是一個對象。

Python源碼中的PyFrameObject

對於Python而言,PyFrameObject可不只僅只是相似於x86機器上看到的那個簡簡單單的棧幀,Python中的PyFrameObject實際上包含了更多的信息。

typedef struct _frame {
    PyObject_VAR_HEAD  		/* 可變對象的頭部信息 */
    struct _frame *f_back;      /* 上一級棧幀, 也就是調用者的棧幀 */
    PyCodeObject *f_code;       /* PyCodeObject對象, 經過棧幀對象的f_code能夠獲取對應的PyCodeObject對象 */
    PyObject *f_builtins;       /* builtin命名空間,一個PyDictObject對象 */
    PyObject *f_globals;        /* global命名空間,一個PyDictObject對象 */
    PyObject *f_locals;         /* local命名空間,一個PyDictObject對象  */
    PyObject **f_valuestack;    /* 運行時的棧底位置 */

    PyObject **f_stacktop;      /* 運行時的棧頂位置 */
    PyObject *f_trace;          /* 回溯函數,打印異常棧 */
    char f_trace_lines;         /* 是否觸發每一行的回溯事件 */
    char f_trace_opcodes;       /* 是否觸發每個操做碼的回溯事件 */

    PyObject *f_gen;            /* 是不是生成器 */

    int f_lasti;                /* 上一條指令在f_code中的偏移量 */

    int f_lineno;               /* 當前字節碼對應的源代碼行 */
    int f_iblock;               /* 當前指令在棧f_blockstack中的索引 */
    char f_executing;           /* 當前棧幀是否仍在執行 */
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* 用於try和loop代碼塊 */
    PyObject *f_localsplus[1];  /* 動態內存,維護局部變量+cell對象集合+free對象集合+運行時棧所須要的空間 */
} PyFrameObject;

所以咱們看到,Python會根據PyCodeObject對象來建立一個棧幀對象(或者直接說棧幀也行),也就是PyFrameObject對象,虛擬機其實是在PyFrameObject對象上執行操做的。每個PyFrameObject都會維護一個PyCodeObject,換句話說,每個PyCodeObject都會隸屬於一個PyFrameObject。而且從f_back中能夠看出,在Python的實際執行過程當中,會產生不少PyFrameObject對象,而這些對象會被連接起來,造成一條執行環境鏈表,這正是x86機器上棧幀之間關係的模擬。在x86機器上,棧幀間經過rsp和rbp指針創建了聯繫,使得新棧幀在結束以後可以順利的返回到舊棧幀中,而Python則是利用f_back來完成這個動做。

裏面f_code成員是一個指針,指向相應的PyCodeObject對象,而接下來的f_builtins、f_globals、f_locals是三個獨立的名字空間,在這裏咱們看到了名字空間和執行環境(即棧幀)之間的關係。名字空間其實是維護這變量名和變量值的PyDictObject對象,因此在這三個PyDictObject對象中分別維護了各自name和value的對應關係。

在PyFrameObject的開頭,有一個PyObject_VAR_HEAD,表示棧幀是一個變長對象,即每一次建立PyFrameObject對象大小多是不同的,那麼變更在什麼地方呢?首先每個PyFrameObject對象都維護了一個PyCodeObject對象,而每個PyCodeObject對象都會對應一個代碼塊(code block)。在編譯一段代碼塊的時候,會計算這段代碼塊執行時所須要的棧空間的大小,這個棧空間大小存儲在PyCodeObject對象的co_stacksize中。而不一樣的代碼塊所須要的棧空間是不一樣的,所以PyFrameObject的開頭要有一個PyObject_VAR_HEAD對象。最後其實PyFrameObject裏面的內存空間分爲兩部分,一部分是編譯代碼塊須要的空間,另外一部分是計算所須要的空間,咱們也稱之爲"運行時棧"。

注意:x86機器上執行時的運行時棧不止包含了計算(還有別的)所須要的內存空間,但PyFrameObject對象的運行時棧則只包含計算所須要的內存空間,這一點務必注意。

在python中訪問PyFrameObject對象

在Python中獲取棧幀,咱們可使用inspect模塊。

import inspect


def f():
    # 返回當前所在的棧幀, 這個函數其實是調用了sys._getframe(1)
    return inspect.currentframe()


frame = f()
print(frame)  # <frame at 0x000001FE3D6E69F0, file 'D:/satori/1.py', line 6, code f>
print(type(frame))  # <class 'frame'>

咱們看到棧幀的類型是<class 'frame'>,正如PyCodeObject對象的類型是<class 'code'>同樣。仍是那句話,這兩個類Python解釋器沒有暴露給咱們,因此不能夠直接使用。同理,還有Python的函數,類型是<class 'function'>;模塊,類型是<class 'module'>,這些Python解釋器都沒有給咱們提供,若是直接使用的話,那麼frame、code、function、module只是幾個沒有定義的變量罷了,這些類咱們只能經過這種間接的方式獲取。

下面咱們就來獲取一下棧幀的成員屬性

import inspect


def f():
    name = "夏色祭"
    age = -1
    return inspect.currentframe()


def g():
    name = "神樂mea"
    age = 38
    return f()


# 當咱們調用函數g的時候, 也會觸發函數f的調用
# 而一旦f執行完畢, 那麼f對應的棧幀就被全局變量frame保存起來了
frame = g()

print(frame)  # <frame at 0x00000194046863C0, file 'D:/satori/1.py', line 8, code f>

# 獲取上一級棧幀, 即調用者的棧幀, 顯然是g的棧幀
print(frame.f_back)  # <frame at 0x00000161C79169F0, file 'D:/satori/1.py', line 14, code g>

# 模塊也是有棧幀的, 咱們後面會單獨說
print(frame.f_back.f_back)  # <frame at 0x00000174CE997840, file 'D:/satori/1.py', line 25, code <module>>
# 顯然最外層就是模塊了, 模塊對應的上一級棧幀是None
print(frame.f_back.f_back.f_back)  # None

# 獲取PyCodeObject對象
print(frame.f_code)  # <code object f at 0x00000215D560D450, file "D:/satori/1.py", line 4>
print(frame.f_code.co_name)  # f

# 獲取f_locals, 即棧幀內部的local名字空間
print(frame.f_locals)  # {'name': '夏色祭', 'age': -1}
print(frame.f_back.f_locals)  # {'name': '神樂mea', 'age': 38}
"""
另外咱們看到函數運行完畢以後裏面的局部變量竟然還能獲取
緣由就是棧幀沒被銷燬, 由於它被返回了, 並且被外部變量接收了
同理:該棧幀的上一級棧幀也不能被銷燬, 由於當前棧幀的f_back指向它了, 引用計數不爲0, 因此要保留
"""

# 獲取棧幀對應的行號
print(frame.f_lineno)  # 8
print(frame.f_back.f_lineno)  # 14
"""
行號爲8的位置是: return inspect.currentframe()
行號爲14的位置是: return f()
"""

經過棧幀咱們能夠獲取不少的屬性,咱們後面還會慢慢說。

此外,異常處理也能夠獲取到棧幀。

def foo():
    try:
        1 / 0
    except ZeroDivisionError:
        import sys
        # exc_info返回一個三元組,分別是異常的類型、值、以及traceback
        exc_type, exc_value, exc_tb = sys.exc_info()
        print(exc_type)  # <class 'ZeroDivisionError'>
        print(exc_value)  # division by zer
        print(exc_tb)  # <traceback object at 0x00000135CEFDF6C0>
        
        # 調用exc_tb.tb_frame便可拿到異常對應的棧幀
        # 另外這個exc_tb也能夠經過except ZeroDivisionError as e; e.__traceback__的方式獲取
        print(exc_tb.tb_frame.f_back)  # <frame at 0x00000260C1297840, file 'D:/satori/1.py', line 17, code <module>>
        # 由於foo是在模塊級別、也就是最外層調用的,因此tb_frame是當前函數的棧幀、那麼tb_frame.f_back就是整個模塊對應的棧幀
        # 那麼再上一級的話, 棧幀就是None了
        print(exc_tb.tb_frame.f_back.f_back)  # None


foo()

名字、做用域、名字空間

咱們在PyFrameObject裏面看到了3個獨立的名字空間:f_locals、f_globals、f_builtins。名字空間對於Python來講是一個很是重要的概念,整個Python虛擬機運行的機制和名字空間有着很是緊密的聯繫。而且在Python中,與命名空間這個概念緊密聯繫着的還有"名字"、"做用域"這些概念,下面就來剖析這些概念是如何實現的。

Python中的變量只是一個名字

很早的時候咱們就說過,Python中的變量在底層一個泛型指針PyObject *,而在Python的層面上來講,變量只是一個名字、或者說符號,用於和對象進行綁定的。變量的定義本質上就是創建名字和對象之間的約束關係,因此a = 1這個賦值語句本質上就是將符號a和1對應的PyLongObject綁定起來,讓咱們經過a能夠找到對應的PyLongObject。

除了變量賦值,函數定義、類定義也至關於定義變量,或者說完成名字和對象之間的綁定。

def foo(): pass


class A(): pass

定義一個函數也至關於定義一個變量,會先根據函數體建立一個函數對象,而後將名字foo和函數對象綁定起來,因此函數名和函數體之間是分離的,同理類也是如此。

再有導入一個模塊,也至關於定義一個變量。

import os

import os,至關於將名字os和模塊對象綁定起來,經過os能夠訪問模塊裏面的屬性。或者import numpy as np當中的as語句也至關於定義一個變量,將名字np和對應的模塊對象綁定起來,之後就能夠經過np這個名字去訪問模塊內部的屬性了。

另外,當咱們導入一個模塊的時候,解釋器是這麼作的。好比:import os等價於os = __import__("os"),能夠看到本質上仍是一個賦值語句。

做用域和名字空間

咱們說賦值語句、函數定義、類定義、模塊導入,本質上只是完成了名字和對象之間的綁定。而從概念上將,咱們實際上獲得了一個nameobj這樣的映射關係,經過name獲取對應的obj,而它們的容身之所就是名字空間。而名字空間是經過PyDictObject對象實現的,這對於映射來講簡直再適合不過了,因此字典在Python底層也是被大量使用的,所以是通過高度優化的。

可是一個模塊內部,名字還存在可見性的問題,好比:

a = 1

def foo():
    a = 2
    print(a)  # 2

foo()
print(a)  # 1

咱們看到同一個變量名,打印的確實不一樣的值,說明指向了不一樣的對象。換句話說這兩個變量是在不一樣的名字空間中被建立的,咱們知道名字空間本質上是一個字典,若是二者是在同一個名字空間,那麼因爲字典的key的不重複性,那麼當我進行a=2的時候,會把字典裏面key爲'a'的value給更新掉,可是在外面仍是打印爲1,這說明,二者所在的不是同一個名字空間。在不一樣的名字空間,打印的也就天然不是同一個a。

所以對於一個模塊而言,內部是可能存在多個名字空間的,每個名字空間都與一個做用域相對應。做用域就能夠理解爲一段程序的正文區域,在這個區域裏面定義的變量是有做用的,然而一旦出了這個區域,就無效了。

對於做用域這個概念,相當重要的是要記住它僅僅是由源程序的文本所決定的。在Python中,一個變量在某個位置是否起做用,是由其在文本位置是否惟一決定的。所以,Python是具備靜態做用域(詞法做用域)的,而名字空間則是做用域的動態體現。一個由程序文本定義的做用域在Python運行時就會轉化爲一個名字空間、即一個PyDictObject對象。也就是說,在函數執行時,會爲建立一個名字空間,這一點在之後剖析函數時會詳細介紹。

咱們以前說Python在對Python源代碼進行編譯的時候,對於代碼中的每個block,都會建立一個PyCodeObject與之對應。而當進入一個新的名字空間、或者說做用域時,咱們就算是進入了一個新的block了。相信此刻你已經明白了,並且根據咱們使用Python的經驗,顯然函數、類都是一個新的block,當Python運行的時候會它們建立各自的名字空間。

因此名字空間是名字、或者變量的上下文環境,名字的含義取決於命名空間。更具體的說,一個變量名對應的變量值什麼,在Python中是不肯定的,須要名字空間來決定。

位於同一個做用域中的代碼能夠直接訪問做用域中出現的名字,即所謂的"直接訪問",也就是不須要經過屬性引用的訪問修飾符:.

class A:
    a = 1


class B:
    b = 2
    print(A.a)  # 1
    print(b)  # 2

好比:B裏面想訪問A裏面的內容,好比經過A.屬性的方式,表示經過A來獲取A裏面的屬性。可是訪問B的內容就不須要了,由於都是在同一個做用域,因此直接訪問便可。

訪問名字這樣的行爲被稱爲名字引用,名字引用的規則決定了Python程序的行爲。

a = 1

def foo():
    a = 2
    print(a)  # 2

foo()
print(a)  # 1

仍是對於上面的代碼,若是咱們把函數裏面的a=2給刪掉,那麼顯然做用域裏面已經沒有a這個變量的,那麼再執行程序會有什麼後果呢?從Python層面來看,顯然是會尋找外部的a。所以咱們能夠獲得以下結論:

  • 做用域是層層嵌套的,顯然是這樣,畢竟python虛擬機操做的是PyFrameObject對象,而PyFrameObject對象也是嵌套的,固然還有PyCodeObject
  • 內層的做用域是能夠訪問外層做用域的
  • 外層做用域沒法訪問內層做用域,儘管咱們沒有試,可是想都不用想,若是把外層的a=1給去掉,那麼最後面的print(a)鐵定報錯。由於外部的做用域算是屬於頂層了(先不考慮builtin)
  • 查找元素會依次從當前做用域向外查找,也就是查找元素對應的做用域是按照從小往大、從裏往外的方向前進的,到了最外層尚未,就真沒有了(先不考慮builtin)

LGB規則

咱們說函數、類是有本身的做用域的,可是模塊對應的源文件自己也有相應的做用域。好比:

# a.py
name = "夏色祭"
age = -1


def foo():
    return 123

class A:
    pass

因爲這個文件自己也有本身的做用域(顯然是global做用域),因此Python解釋器在運行a.py這個文件的時候,也會爲其建立一個名字空間,而顯然這個名字空間就是global名字空間。它裏面的變量是全局的,或者說是模塊級別的,在當前的文件內能夠直接訪問。

而函數也會有一個做用域,這個做用域稱爲local做用域(對應local名字空間);同時Python自身還定義了一個最頂層的做用域,也就是builtin做用域(好比:dir、range、open都是builtin裏面的)。這三個做用域在python2.2以前就存在了,因此那時候Python的做用域規則被稱之爲LGB規則:名字引用動做沿着local做用域(local名字空間)、global做用域(global名字空間)、builtin做用域(builtin名字空間)來查找對應的變量。

而獲取名字空間,Python也提供了相應的內置函數:

  • locals函數: 獲取當前做用域的local名字空間, local名字空間也稱爲局部名字空間
  • globals函數: 獲取當前做用域的global名字空間, global名字空間也稱爲全局名字空間

對於global名字空間來講,它對應一個字典,而且這個字典是全局惟一的,全局變量都存儲在這裏面。

name = "夏色祭"
age = -1


def foo():
    name = "神樂mea"
    age = 38


print(globals())  # {..., 'name': '夏色祭', 'age': -1, 'foo': <function foo at 0x0000020BF60851F0>}

裏面的...表示省略了一部分輸出,咱們看到建立的全局變量都在裏面了。並且foo也是一個變量,它指向一個函數對象,咱們說foo也對應一個PyCodeObject。可是在解釋到def foo的時候,便會根據這個PyCodeObject對象建立一個PyFunctionObject對象,而後將foo和這個函數對象綁定起來。當咱們調用foo的時候,會根據PyFunctionObject對象再建立PyFrameObject對象、而後執行,這些留在介紹函數的時候再細說。總之,咱們看到foo也是一個全局變量,全局變量都在global名字空間中。

global名字空間全局惟一,它是程序運行時全局變量和與之綁定的對象的容身之所,你在任何一個地方均可以訪問到global名字空間。正如,你在任何一個地方均可以訪問相應的全局變量同樣。

此外,咱們說名字空間是一個字典,變量和變量指向的值會以鍵值對的形式存在裏面。那麼換句話說,若是我手動的往這個global名字空間裏面添加一個鍵值對,是否是也等價於定義一個全局變量呢?

globals()["name"] = "夏色祭"
print(name)  # 夏色祭


def f1():
    def f2():
        def f3():
            globals()["age"] = -1
        return f3
    return f2


f1()()()
print(age)  # -1

咱們看到確實如此,經過往global名字空間裏面插入一個鍵值對徹底等價於定義一個全局變量。而且咱們看到global名字空間是全局惟一的,你在任何地方調用globals()獲得的都是global名字空間,正如你在任意地方均可以訪問到全局變量同樣。因此即便是在函數中向global名字空間中插入一個鍵值對,也等價於定義一個全局變量、並和對象綁定起來。

  • name = "夏色祭"等價於 globals["name"] = "夏色祭"
  • print(name)等價於print(globals["name"])

對於local名字空間來講,它也對應一個字典,顯然這個字典是就不是全局惟一的了,每個做用域都會對應自身的local名字空間。

def f():
    name = "夏色祭"
    age = -1
    return locals()


def g():
    name = "神樂mea"
    age = 38
    return locals()


print(locals() == globals())  # True
print(f())  # {'name': '夏色祭', 'age': -1}
print(g())  # {'name': '神樂mea', 'age': 38}

顯然對於模塊來說,它的local名字空間和global名字空間是同樣的,也就是說模塊對應的PyFrameObject對象裏面的f_locals和f_globals指向的是同一個PyDictObject對象。

可是對於函數而言,局部名字空間和全局名字空間就不同了。而調用locals也是獲取自身的局部名字空間,所以不一樣的函數的local名字空間是不一樣的,而調用locals函數返回結果顯然取決於調用它的位置。可是globals函數的調用結果是同樣的,獲取的都是global名字空間,這也符合"函數內找不到某個變量的時候會去找全局變量"這一結論。

因此咱們說在函數裏面查找一個變量,查找不到的話會找全局變量,全局變量再沒有會查找內置變量。本質上就是按照自身的local空間、外層的global空間、內置的builtin空間的順序進行查找。所以local空間會有不少個,由於每個函數或者類都有本身的局部做用域,這個局部做用域就能夠稱之爲該函數的local空間;可是global空間則全局惟一,由於該字典存儲的是全局變量,不管你在什麼地方,經過globals拿到的永遠全局變量對應的名字空間,向該空間中添加鍵值對,等價於建立全局變量。

對於builtin命名空間,它也是一個字典。當local空間、global空間都沒有的時候,會去builtin空間查找。

name = "夏色祭"
age = -1


def f1():
    name = "神樂mea"
    # local空間有"name"這個key, 直接從局部名字空間獲取
    print(name)
    # 可是當前的local空間沒有"age"這個key, 因此會從global空間查找
    # 從這裏也能看出爲何函數也能訪問到global空間了
    # 若是函數內訪問不到的話, 那麼它怎麼可以在局部變量找不到的時候去找全局變量呢
    print(age)

    # 可是local空間、global空間都沒有"int"這個key, 因此要去builtin空間查找了
    print(int)

    # "xxx"的話, 三個空間都沒有, 那麼結果只能是NameError了
    print(xxx)


f1()
"""
神樂mea
-1
<class 'int'>

...
File "D:/satori/1.py", line 18, in f1
    print(xxx)
NameError: name 'xxx' is not defined
"""

問題來了,builtin名字空間如何獲取呢?答案是經過builtins模塊。

import builtins

# 咱們調用int、str、list顯然是從內置做用域、也就是builtin命名空間中查找的
# 即便咱們只經過list也是能夠的, 由於local空間、global空間沒有的話, 最終會從builtin空間中查找,
# 但若是是builtins.list, 那麼就不兜圈子了, 表示: "builtin空間,就從你這獲取了"
print(builtins.list is list)  # True

builtins.dict = 123
# 將builtin空間的dict改爲123,那麼此時獲取的dict就是123,由於是從內置做用域中獲取的
print(dict + 456)  # 579

str = 123
# 若是是str = 123,等價於建立全局變量str = 123,顯然影響的是global空間,而查找顯然也會先從global空間查找
print(str)  # 123
# 可是此時不影響內置做用域
print(builtins.str)  # <class 'str'>

這裏提一下Python2當中,while 1比while True要快,爲何?

由於True在Python2中不是關鍵字,因此它是能夠做爲變量名的,那麼python在執行的時候就要先看local空間和global空間中有沒有True這個變量,有的話使用咱們定義的,沒有的話再使用內置的True,而1是一個常量直接加載就能夠。因此while True它多了符號查找這一過程,可是在Python3中二者就等價了,由於True在python3中是一個關鍵字,因此會直接做爲一個常量來加載。

這裏再提一下函數的local空間

咱們說:globals["name"] = "夏色祭"等價於定義一個全局變量name = "夏色祭",那麼若是是在函數裏面執行了locals["name"] = "夏色祭",是否是等價於建立局部變量name = "夏色祭"呢?

def f1():
    locals()["name "] = "夏色祭"
    try:
        print(name)
    except Exception as e:
        print(e)

f1()  # name 'name' is not defined

咱們說對於全局變量來說,變量的建立是經過向字典添加鍵值對的方式實現的。由於全局變量會一直在變,須要使用字典來動態維護。可是對於函數來說,內部的變量是經過靜態方式訪問的,由於其局部做用域中存在哪些變量在編譯的時候就已經肯定了,咱們經過PyCodeObject的co_varnames便可獲取內部都有哪些變量。

因此雖然咱們說查找是按照LGB的方式查找,可是訪問函數內部的變量實際上是靜態訪問的,不過徹底能夠按照LGB的方式理解。

因此名字空間能夠說是Python的靈魂,由於它規定了Python變量的做用域,使得Python對變量的查找變得很是清晰。

LEGB規則

咱們上面說的LGB是針對Python2.2以前的,那麼Python2.2開始,因爲引入了嵌套函數,顯然最好的方式應該是內層函數找不到應該首先去外層函數找,而不是直接就跑到global空間、也就是全局裏面找,那麼此時的規則就是LEGB。

a = 1

def foo():
    a = 2

    def bar():
        print(a)
    return bar


f = foo()
f()
"""
2
"""

調用f,實際上調用的是bar函數,最終輸出的結果是2。若是按照LGB的規則來查找的話。bar函數的做用域沒有a、那麼應該到全局裏面找,打印的應該是1纔對。可是咱們以前說了,做用域僅僅是由文本決定的,函數bar位於函數foo以內,因此bar函數定義的做用域內嵌與函數foo的做用域以內。換句話說,函數foo的做用域是函數bar的做用域的直接外圍做用域,因此首先是從foo做用域裏面找,若是沒有那麼再去全局裏面找。而做用域和名字空間是對應的,因此最終打印了2。

所以在執行f = foo()的時候,會執行函數foo中的def bar():語句,這個時候Python會將a=2與函數bar對應的函數對象捆綁在一塊兒,將捆綁以後的結果返回,這個捆綁起來的總體稱之爲閉包。

因此:閉包 = 內層函數 + 引用的外層做用域

這裏顯示的規則就是LEGB,其中E成爲enclosing,表明直接外圍做用域這個概念。

global表達式

有一個很奇怪的問題,最開始學習python的時候,筆者也爲此困惑了一段時間,下面咱們來看一下。

a = 1

def foo():
    print(a)

foo()
"""
1
"""

首先這段代碼打印1,這顯然是沒有問題的,可是下面問題來了。

a = 1

def foo():
    print(a)
    a = 2

foo()
"""
Traceback (most recent call last):
  File "C:/Users/satori/Desktop/love_minami/a.py", line 8, in <module>
    foo()
  File "C:/Users/satori/Desktop/love_minami/a.py", line 5, in foo
    print(a)
UnboundLocalError: local variable 'a' referenced before assignment
"""

這裏我僅僅是在print下面,在當前做用域又新建了一個變量a,結果就告訴我局部變量a在賦值以前就被引用了,這是怎麼一回事,相信確定有人爲此困惑。

弄明白這個錯誤的根本就在於要深入理解兩點:

  • 一個賦值語句所定義的變量在這個賦值語句所在的做用域裏都是可見的
  • 函數中的變量是靜態存儲、靜態訪問的, 內部有哪些變量在編譯的時候就已經肯定

在編譯的時候,由於存在a = 2這條語句,因此知道函數中存在一個局部變量a,那麼查找的時候就會在局部空間中查找。可是還沒來得及賦值,就print(a)了,因此報錯:局部變量a在賦值以前就被引用了。但若是沒有a = 2這條語句則不會報錯,由於知道局部做用域中不存在a這個變量,因此會找全局變量a,從而打印1。

更有趣的東西隱藏在字節碼當中,咱們能夠經過反彙編來查看一下:

import dis

a = 1


def g():
    print(a)

dis.dis(g)
"""
  7           0 LOAD_GLOBAL              0 (print)
              2 LOAD_GLOBAL              1 (a)
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
"""

def f():
    print(a)
    a = 2

dis.dis(f)
"""
 12           0 LOAD_GLOBAL              0 (print)
              2 LOAD_FAST                0 (a)
              4 CALL_FUNCTION            1
              6 POP_TOP

 13           8 LOAD_CONST               1 (2)
             10 STORE_FAST               0 (a)
             12 LOAD_CONST               0 (None)
             14 RETURN_VALUE
"""

中間的序號表明字節碼的偏移量,咱們看第二條,g的字節碼是LOAD_GLOBAL,意思是在global名字空間中查找,而f的字節碼是LOAD_FAST,表示在local名字空間中查找名字。這說明Python採用了靜態做用域策略,在編譯的時候就已經知道了名字藏身於何處。

所以上面的例子代表,一旦做用域有了對某個名字的賦值操做,這個名字就會在做用域中可見,就會出如今local名字空間中,換句話說,就遮蔽了外層做用域中相同的名字。

但有時咱們想要在函數裏面修改全局變量呢?固然Python也爲咱們精心準備了global關鍵字,好比函數內部出現了global a,就表示我後面的a是全局的,你要到global名字空間裏面找,不要在local空間裏面找了

a = 1

def bar():
    def foo():
        global a
        a = 2
    return foo

bar()()
print(a)  # 2

可是若是外層函數裏面也出現了a,咱們想找外層函數裏面的a而不是全局的a,該怎麼辦呢?Python一樣爲咱們準備了關鍵字: nonlocal,可是nonlocal的時候,必須確保本身是內層函數。

a = 1

def bar():
    a = 2
    def foo():
        nonlocal a
        a = "xxx"
    return foo

bar()()
print(a)  # 1
# 外界依舊是1

屬性引用與名稱引用

屬性引用實質上也是一種名稱引用,其本質都是到名稱空間中去查找一個名稱所引用的對象。這個就比較簡單了,好比a.xxx,就是到a裏面去找xxx,這個規則是不受LEGB做用域限制的,就是到a裏面查找,有就是有、沒有就是沒有。

這個比較簡單,可是有一點咱們須要注意,那就是咱們說屬性查找會按照LEGB的規則,可是僅僅限制在自身所在的模塊內。舉個栗子:

# a.py
print(name)
# b.py
name = "夏色祭"
import a

關於模塊的導入咱們後面系列中會詳細說,總之目前在b.py裏面執行的import a,你能夠簡單認爲就是把a.py裏面的內容拿過來執行一遍便可,因此這裏至關於print(name)。

可是執行b.py的時候會提示變量name沒有被定義,但是把a導進來的話,就至關於print(name),而咱們上面也定義name這個變量了呀。顯然,即便咱們把a導入了進來,可是a.py裏面的內容依舊是處於一個模塊裏面。而咱們也說了,名稱引用雖然是LEGB規則,可是不管如何都沒法越過自身的模塊的,print(name)是在a.py裏面的,而變量name被定義在b.py中,因此是不可能跨過模塊a的做用域去訪問模塊b裏面的內容的。

因此模塊總體也有一個做用域,就是該模塊的全局做用域,每一個模塊是相互獨立的。因此咱們發現每一個模塊之間做用域仍是劃分的很清晰的,都是相互獨立的。

關於模塊,咱們後續會詳細說。總之經過.的方式本質上都是去指定的命名空間中查找對應的屬性。

屬性空間

咱們知道,自定義的類中若是沒有__slots__,那麼這個類的實例對象都會有一個屬性字典。

class Girl:

    def __init__(self):
        self.name = "夏色祭"
        self.age = -1


g = Girl()
print(g.__dict__)  # {'name': '夏色祭', 'age': -1}

# 對於查找屬性而言, 也是去屬性字典中查找
print(g.name, g.__dict__["name"])

# 同理設置屬性, 也是更改對應的屬性字典
g.__dict__["gender"] = "female"
print(g.gender)  # female

固然模塊也有屬性字典,屬性查找方面,本質上和上面的類的實例對象是一致的。

import builtins

print(builtins.str)  # <class 'str'>
print(builtins.__dict__["str"])  # <class 'str'>

另外global空間裏面是保存了builtin空間的指針的:

# globals()["__builtins__"]直接等價於import builtins
print(globals()["__builtins__"])  # <module 'builtins' (built-in)>

import builtins
print(builtins)  # <module 'builtins' (built-in)>

# 但咱們說globals函數是在什麼地方呢? 顯然是在builtin空間中
# 因此
print(globals()["__builtins__"].globals()["__builtins__"].
      globals()["__builtins__"].globals()["__builtins__"].
      globals()["__builtins__"].globals()["__builtins__"])  # <module 'builtins' (built-in)>

print(globals()["__builtins__"].globals()["__builtins__"].
      globals()["__builtins__"].globals()["__builtins__"].
      globals()["__builtins__"].globals()["__builtins__"].list("abc"))  # ['a', 'b', 'c']

小結

Python 中,一個名字(變量)可見範圍由 "做用域" 決定,而做用域由語法靜態劃分,劃分規則提煉以下:

  • .py文件(模塊)最外層爲全局做用域
  • 遇到函數定義,函數體造成子做用域
  • 遇到類定義,類定義體造成子做用域
  • 名字僅在其做用域之內可見
  • 全局做用域對其餘全部做用域可見
  • 函數做用域對其直接子做用域可見,而且能夠傳遞(閉包)

與"做用域"相對應, Python 在運行時藉助 PyDictObject 對象保存做用域中的名字,構成動態的"名字空間" 。這樣的名字空間總共有 4 個:

  • 局部名字空間(builtin): 不一樣的函數,局部名字空間不一樣
  • 全局名字空間(global): 全局惟一
  • 閉包名字空間(enclosing)
  • 內建名字空間(builtin)
  • 在查找名字時會按照LEGB規則查找, 可是注意: 沒法跨越文件自己。就是按照自身文件的LEGB, 若是屬性查找都找到builtin空間了, 那麼證實這已是最後的倔強。若是builtin空間再找不到, 那麼就只能報錯了, 不可能跑到其它文件中找

python虛擬機的運行框架

當Python啓動後,首先會進行運行時環境的初始化。注意這裏的運行時環境,它和上面說的執行環境是不一樣的概念。運行時環境是一個全局的概念,而執行時環境是一個棧幀,是一個與某個code block相對應的概念。如今不清楚二者的區別沒關係,後面會詳細介紹。關於運行時環境的初始化是一個很是複雜的過程,咱們後面將用單獨的一章進行剖析,這裏就假設初始化動做已經完成,咱們已經站在了Python虛擬機的門檻外面,只須要輕輕推進一下第一張骨牌,整個執行過程就像多米諾骨牌同樣,一環扣一環地展開。

首先Python虛擬機執行PyCodeObject對象中字節碼的代碼爲Python/ceval.c中,主要函數有兩個:PyEval_EvalCodeEx 是通用接口,通常用於函數這樣帶參數的執行場景; PyEval_EvalCode 是更高層封裝,用於模塊等無參數的執行場景。

PyObject *
PyEval_EvalCode(PyObject *co, PyObject *globals, PyObject *locals);

PyObject *
PyEval_EvalCodeEx(PyObject *_co, PyObject *globals, PyObject *locals,
                  PyObject *const *args, int argcount,
                  PyObject *const *kws, int kwcount,
                  PyObject *const *defs, int defcount,
                  PyObject *kwdefs, PyObject *closure);

這兩個函數最終調用 _PyEval_EvalCodeWithName 函數,初始化棧幀對象並調用PyEval_EvalFrame 和PyEval_EvalFrameEx函數進行處理。棧幀對象將貫穿代碼對象執行的始終,負責維護執行時所需的一切上下文信息。而PyEval_EvalFramePyEval_EvalFrameEx函數最終調用 _PyEval_EvalFrameDefault 函數,虛擬機執行的祕密就藏在這裏。

PyObject *
PyEval_EvalFrame(PyFrameObject *f);
PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag);

_PyEval_EvalFrameDefault函數是虛擬機運行的核心,這一個函數加上註釋大概在3100行左右。能夠說代碼量很是大,可是邏輯並不難理解。

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{	
    /*
    該函數首先會初始化一些變量,PyFrameObject對象中的PyCodeObject對象包含的信息不用說,還有一個重要的動做就是初始化堆棧的棧頂指針,使其指向f->f_stacktop
    */
    //......
    co = f->f_code;
    names = co->co_names;
    consts = co->co_consts;
    fastlocals = f->f_localsplus;
    freevars = f->f_localsplus + co->co_nlocals;
    next_instr = first_instr;
    if (f->f_lasti >= 0) {
        assert(f->f_lasti % sizeof(_Py_CODEUNIT) == 0);
        next_instr += f->f_lasti / sizeof(_Py_CODEUNIT) + 1;
    }
    stack_pointer = f->f_stacktop;
    assert(stack_pointer != NULL);
    f->f_stacktop = NULL;       
    //......
}
    /*
    PyFrameObject對象中的f_code就是PyCodeObject對象,而PyCodeObject對象裏面的co_code域則保存着字節碼指令和字節碼指令參數
    python執行字節碼指令序列的過程就是從頭至尾遍歷整個co_code、依次執行字節碼指令的過程。在Python的虛擬機中,利用三個變量來完成整個遍歷過程。
    首先co_code本質上是一個PyBytesObject對象,而其中的字符數組纔是真正有意義的東西。也就是說整個字節碼指令序列就是c中一個普普統統的數組。
    所以遍歷的過程使用的3個變量都是char *類型的變量
    1.first_instr:永遠指向字節碼指令序列的開始位置
    2.next_instr:永遠指向下一條待執行的字節碼指令的位置
    3.f_lasti:指向上一條已經執行過的字節碼指令的位置
    */

那麼這個一步一步的動做是如何完成的呢?其實就是一個for循環加上一個巨大的switch case結構。

PyObject* _Py_HOT_FUNCTION
_PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
{   
    //......   
    co = f->f_code;
    names = co->co_names;
    consts = co->co_consts;
    fastlocals = f->f_localsplus;
    freevars = f->f_localsplus + co->co_nlocals;
    //......
	
    // 逐條取出字節碼來執行
    for (;;) {
        if (_Py_atomic_load_relaxed(eval_breaker)) {
            // 讀取下條字節碼
            // 字節碼位於: f->f_code->co_code, 偏移量由 f->f_lasti 決定
            opcode = _Py_OPCODE(*next_instr);
            //opcode是指令,咱們說Python在Include/opcode.h中定義了121個指令
            if (opcode == SETUP_FINALLY ||
                opcode == SETUP_WITH ||
                opcode == BEFORE_ASYNC_WITH ||
                opcode == YIELD_FROM) {
                goto fast_next_opcode; 
            }

        fast_next_opcode:
            //......
            //判斷該指令屬於什麼操做,而後執行相應的邏輯
            switch (opcode) {
                // 加載常量
                case LOAD_CONST:
                    // ....
                    break;
                // 加載名字
                case LOAD_NAME:
                    // ...
                    break;
                // ...
        }
    }
}

在這個執行架構中,對字節碼一步一步的遍歷是經過幾個宏來實現的:

#define INSTR_OFFSET()  \
    (sizeof(_Py_CODEUNIT) * (int)(next_instr - first_instr))

#define NEXTOPARG()  do { \
        _Py_CODEUNIT word = *next_instr; \
        opcode = _Py_OPCODE(word); \
        oparg = _Py_OPARG(word); \
        next_instr++; \
    } while (0)

Python的字節碼有的是帶有參數的,有的是沒有參數的,而判斷字節碼是否帶有參數是經過HAS_AGR這個宏來實現的。注意:對於不一樣的字節碼指令,因爲存在是否須要指令參數的區別,因此next_instr的位移能夠是不一樣的,但不管如何,next_instr老是指向python下一條要執行的字節碼。

Python在得到了一條字節碼指令和其須要的參數指令以後,會對字節碼利用switch進行判斷,根據判斷的結果選擇不一樣的case語句,每一條指令都會對應一個case語句。在case語句中,就是Python對字節碼指令的實現。因此這個switch語句很是的長,函數總共3000行左右,這個switch就佔了2400行,由於指令有121個,好比:LOAD_CONST、LOAD_NAME、YIELD_FROM等等,而每個指令都要對應一個case語句。

在成功執行完一條字節碼指令和其須要的指令參數以後,Python的執行流程會跳轉到fast_next_opcode處,或者for循環處。無論如何,Python接下來的動做就是獲取下一條字節碼指令和指令參數,完成對下一條指令的執行。經過for循環一條一條地遍歷co_code中包含的全部字節碼指令,而後交給for循環裏面的switch語句,如此周而復始,最終完成了對Python程序的執行。

儘管只是簡單的分析,可是相信你們也能瞭解Python執行引擎的大致框架,在Python的執行流程進入了那個巨大的for循環,取出第一條字節碼交給裏面的switch語句以後,第一張多米諾骨牌就已經被推倒,命運不可阻擋的降臨了。一條接一條的字節碼像潮水同樣涌來,浩浩蕩蕩,橫無際涯。

咱們這裏經過反編譯的方式演示一下

指令分爲不少種,咱們這裏就以簡單的順序執行爲例,不涉及任何的跳轉指令,看看Python是如何執行字節碼的。

pi = 3.14
r = 3
area = pi * r ** 2

對它們反編譯以後,獲得的字節碼指令以下:

1           0 LOAD_CONST               0 (3.14)
              2 STORE_NAME               0 (pi)

  2           4 LOAD_CONST               1 (3)
              6 STORE_NAME               1 (r)

  3           8 LOAD_NAME                0 (pi)
             10 LOAD_NAME                1 (r)
             12 LOAD_CONST               2 (2)
             14 BINARY_POWER
             16 BINARY_MULTIPLY
             18 STORE_NAME               2 (area)
             20 LOAD_CONST               3 (None)
             22 RETURN_VALUE

第一列是源代碼的行號,第二列是指令的偏移量(或者說指令對應的索引),第三行是操做數(或者操做碼, 它們在宏定義中表明整數),第四行的含義咱們具體分析的時候說(至於後面的括號則至關於一個提示)

  • 0 LOAD_CONST: 表示加載一個常量(壓入"運行時棧"),後面的0 (3.14)表示從常量池中加載索引爲0的對象,3.14表示加載的對象是3.14(因此最後面的括號裏面的內容實際上起到的是一個提示做用,告訴你加載的對象是什麼)。
  • 2 STORE_NAME: 表示將LOAD_CONST獲得的對象用一個名字存儲、或者綁定起來。0 (pi)表示使用符號表(co_varnames)中索引爲0的名字(符號),且名字爲"pi"。
  • 4 LOAD_CONST和6 STORE_NAME顯然和上面是同樣的,只不事後面的索引變成了1,表示加載常量池中索引爲1的對象、符號表中索引爲1的符號(名字)。另外從這裏咱們也能看出,一行賦值語句實際上對應兩條字節碼(加載常量、與名字綁定)
  • 8 LOAD_NAME表示加載符號表中pi對應的值,10 LOAD_NAME表示加載符號表中r對應的值,12 LOAD_CONST表示加載2這個常量2 (2)表示常量池中索引爲2的對象是2
  • 14 BINARY_POWER表示進行冪運算,16 BINARY_MULTIPLY表示進行乘法運算,18 STORE_NAME表示用符號表中索引爲2的符號(area)存儲上一步計算的結果,20 LOAD_CONST表示將None加載進來,22 RETURN_VALUE將None返回。雖然它不是在函數裏面,但也是有這一步的。

咱們經過幾張圖展現一下上面的過程:

Python 虛擬機剛開始執行時,準備好棧幀對象用於保存執行上下文,關係以下(省略部分信息)。另外,圖中有地方畫錯了,圖中的co_varnames應該改爲co_names。咱們說對於函數來講是經過co_varnames獲取符號表(local空間裏面局部變量的存儲位置,一個靜態數組),由於函數有哪些局部變量在編譯時已經肯定,會靜態存儲在符號表co_varnames中。但咱們這裏是對模塊進行反編譯、不是函數,而模塊的符號是全局的,local空間和global空間是同一個,使用字典來維護,因此它的co_varnames是一個空元組。但co_names是能夠獲取到全部的符號的,所以這裏把co_names理解爲符號表便可,但咱們知道全局變量是存在字典裏面的。

因爲 next_instr 初始狀態指向字節碼開頭,虛擬機開始加載第一條字節碼指令: 0 LOAD_CONST 0 (3.14) 。字節碼分爲兩部分,分別是 操做碼 ( opcode )和 操做數 ( oparg ) 。LOAD_CONST 指令表示將常量加載進運行時棧,常量下標由操做數給出。LOAD_CONST 指令在 _PyEval_EvalFrameDefault 函數 switch 結構的一個 case 分支中實現:

TARGET(LOAD_CONST) {
    //經過GETITEM從consts(常量池)中加載索引爲oparg的對象(常量)
    //因此0 LOAD_CONST 0 (3.14)分別表示: 
    //字節碼指令的偏移量、操做數、對象在常量池中的索引(即這裏的oparg)、對象的值(對象的值、或者說常量的值實際上是dis模塊幫你解析出來的)
    PyObject *value = GETITEM(consts, oparg);
    //增長引用計數
    Py_INCREF(value);
    //壓入運行時棧, 這個運行時棧是位於棧幀對象尾部, 咱們一下子會說
    PUSH(value);
    FAST_DISPATCH();
}

接着虛擬機接着執行 2 STORE_NAME 0 (pi) 指令,從符號表中獲取索引爲0的符號、即pi,而後將棧頂元素3.14彈出,再把符號"pi"和整數對象3.14綁定起來保存到local名字空間

case TARGET(STORE_NAME): {
    	    //從符號表中加載索引爲oparg的符號	
            PyObject *name = GETITEM(names, oparg);
    	    //從棧頂彈出元素	
            PyObject *v = POP();
            //獲取名字空間namespace
            PyObject *ns = f->f_locals;
            int err;
            if (ns == NULL) {
                //若是沒有名字空間則報錯, 這個tstate是和線程密切相關的, 咱們後面會說
                _PyErr_Format(tstate, PyExc_SystemError,
                              "no locals found when storing %R", name);
                Py_DECREF(v);
                goto error;
            }
    		//將符號和對象綁定起來放在ns中
            if (PyDict_CheckExact(ns))
                err = PyDict_SetItem(ns, name, v);
            else
                err = PyObject_SetItem(ns, name, v);
            Py_DECREF(v);
            if (err != 0)
                goto error;
            DISPATCH();
        }

你可能會問,變量賦值爲啥不直接經過名字空間,而是到臨時棧繞一圈?主要緣由在於: Python 字節碼只有一個操做數,另外一個操做數只能經過臨時棧給出。 Python 字節碼設計思想跟 CPU精簡指令集相似,指令儘可能簡化,複雜指令由多條指令組合完成。

同理,r = 2對應的兩條指令也是相似的。

而後8 LOAD_NAME 0 (pi)、10 LOAD_NAME 1 (r)、12 LOAD_CONST 2 (2),表示將符號pi指向的值、符號r指向的值、常量2壓入運行時棧。

而後14 BINARY_POWER表示進行冪運算,16 BINARY_MULTIPLY表示進行乘法運算。

其中, BINARY_POWER 指令會從棧上彈出兩個操做數(底數 3 和 指數 2 )進行 冪運算,並將結果 9 壓回棧中; BINARY_MULTIPLY 指令則進行乘積運算 ,步驟也是相似的。

case TARGET(BINARY_POWER): {
    		//從棧頂彈出元素, 這裏是指數2
            PyObject *exp = POP();
            //咱們看到這個是TOP, 因此其實它不是彈出底數3, 而是獲取底數3, 因此3這個元素依舊在棧裏面
            PyObject *base = TOP();
    	    //進行冪運算
            PyObject *res = PyNumber_Power(base, exp, Py_None);
            Py_DECREF(base);
            Py_DECREF(exp);
            //將冪運算的結果再設置回去, 因此原來的3被計算以後的9給替換掉了
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }

        case TARGET(BINARY_MULTIPLY): {
            //同理這裏也是彈出元素9
            PyObject *right = POP();
            //獲取元素3.14
            PyObject *left = TOP();
            //乘法運算
            PyObject *res = PyNumber_Multiply(left, right);
            Py_DECREF(left);
            Py_DECREF(right);
            //將運算的結果28.26將原來的3.14給替換掉
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }

最終執行指令18 STORE_NAME 2 (area),會從符號表中加載索引爲2的符號、即area,再將"area"和浮點數28.26綁定起來放到名字空間中。

總體的執行流程便如上面幾張圖所示,固然字節碼指令有不少,咱們說它們定義在Include/opcode.h中,有121個。好比:除了LOAD_CONST、STORE_NAME以外,還有LOAD_FAST、LOAD_GLOBAL、STORE_FAST,以及if語句、循環語句所使用的跳轉指令,運算使用的指令等等等等,這些在後面的系列中會慢慢遇到。

PyFrameObject中的動態內存空間

上面咱們提到了一個運行時棧,咱們說加載常量的時候會將常量(對象)從常量池中獲取、並壓入運行時棧,當計算或者使用變量保存的時候,會將其從棧裏面彈出來。那麼這個運行時棧所須要的空間都保存在什麼地方呢?

PyFrameObject中有這麼一個屬性f_localsplus(能夠回頭看一下PyFrameObject的定義),咱們說它是動態內存,用於"維護局部變量+cell對象集合+free對象集合+運行時棧所須要的空間",所以能夠看出這段內存不只僅使用來給棧使用的,還有別的對象使用。

PyFrameObject*
PyFrame_New(PyThreadState *tstate, PyCodeObject *code,
            PyObject *globals, PyObject *locals)
{	
    //本質上調用了_PyFrame_New_NoTrack
    PyFrameObject *f = _PyFrame_New_NoTrack(tstate, code, globals, locals);
    if (f)
        _PyObject_GC_TRACK(f);
    return f;
}


PyFrameObject* _Py_HOT_FUNCTION
_PyFrame_New_NoTrack(PyThreadState *tstate, PyCodeObject *code,
                     PyObject *globals, PyObject *locals)
{	
    //上一級的棧幀, PyThreadState指的是線程對象
    PyFrameObject *back = tstate->frame;
    //當前的棧幀
    PyFrameObject *f;
    //builtin
    PyObject *builtins;
	/*
	...
	...
	...
	...
	
	*/
    else {
        Py_ssize_t extras, ncells, nfrees;
        ncells = PyTuple_GET_SIZE(code->co_cellvars);
        nfrees = PyTuple_GET_SIZE(code->co_freevars);
        //這四部分便構成了PyFrameObject維護的動態內存區,其大小由extras肯定
        extras = code->co_stacksize + code->co_nlocals + ncells +
            nfrees;
        
    /*
	...
	...
	...
	...
	
	*/
        f->f_code = code;
        //計算初始化運行時,棧的棧頂,因此沒有加上stacksize
        extras = code->co_nlocals + ncells + nfrees;
        //f_valuestack維護運行時棧的棧底
        f->f_valuestack = f->f_localsplus + extras;
        for (i=0; i<extras; i++)
            f->f_localsplus[i] = NULL;
        f->f_locals = NULL;
        f->f_trace = NULL;
    }
    //f_stacktopk維護運行時棧的棧頂
    f->f_stacktop = f->f_valuestack;
    f->f_builtins = builtins;
    Py_XINCREF(back);
    f->f_back = back;
    Py_INCREF(code);
    Py_INCREF(globals);
    f->f_globals = globals;
    /* Most functions have CO_NEWLOCALS and CO_OPTIMIZED set. */
    if ((code->co_flags & (CO_NEWLOCALS | CO_OPTIMIZED)) ==
        (CO_NEWLOCALS | CO_OPTIMIZED))
        ; /* f_locals = NULL; will be set by PyFrame_FastToLocals() */
    else if (code->co_flags & CO_NEWLOCALS) {
        locals = PyDict_New();
        if (locals == NULL) {
            Py_DECREF(f);
            return NULL;
        }
        f->f_locals = locals;
    }
    else {
        if (locals == NULL)
            locals = globals;
        Py_INCREF(locals);
        f->f_locals = locals;
    }
	
    //設置一些其餘屬性,返回返回該棧幀
    f->f_lasti = -1;
    f->f_lineno = code->co_firstlineno;
    f->f_iblock = 0;
    f->f_executing = 0;
    f->f_gen = NULL;
    f->f_trace_opcodes = 0;
    f->f_trace_lines = 1;

    return f;
}

能夠看到,在建立PyFrameObject對象時,額外申請的"運行時棧"對應的空間並不徹底是給運行時棧使用的,有一部分是給"PyCodeObject對象中存儲的那些局部變量"、"co_freevars"、"co_cellvars"(co_freevars、co_cellvars是與閉包有關的內容,後面章節會剖析)使用的,而剩下的纔是給真正運行時棧使用的。

而且這段連續的空間是由四部分組成,而且順序是"局部變量"、"Cell對象"、"Free對象"、"運行時棧"。

小結

此次咱們深刻了 Python 虛擬機源碼,研究虛擬機執行字節碼的全過程。虛擬機在執行PyCodeObject對象裏面的字節碼以前,須要先根據PyCodeObject對象建立棧幀對象 ( PyFrameObject ),用於維護運行時的上下文信息。而後在PyFrameObject的基礎上,執行字節碼。

PyFrameObject 關鍵信息包括:

  • f_locals: 局部名字空間
  • f_globals: 全局名字空間
  • f_builtins: 內建名字空間
  • f_code: PyCodeObject對象
  • f_lasti: 上條已執行指令的編號, 或者說偏移量、索引均可以
  • f_back: 該棧幀的上一級棧幀、即調用者棧幀
  • f_localsplus: 局部變量 + co_freevars + co_cellvars + 運行時棧, 這四部分須要的空間

棧幀對象經過 f_back 串成一個"棧幀調用鏈",與 CPU 棧幀調用鏈有殊途同歸之妙。咱們還藉助 inspect 模塊成功取得棧幀對象(底層是經過sys模塊),並在此基礎上輸出整個函數調用鏈。

Python虛擬機的代碼量不小,可是核心並不難理解,主要是_PyEval_EvalFrameDefault裏面的一個巨大的for循環,準確的說for循環裏面的那個巨型switch語句。其中的switch語句,case了每個操做指令,當出現什麼指令就執行什麼操做。

另外咱們提到運行時環境,這個運行時環境很是複雜,由於Python啓動是要建立一個主進程、在進程內建立一個主線程的。因此還涉及到了進程和線程的初始化,在後面的系列中咱們會詳細說,包括GIL的問題。這裏咱們就先假設運行時環境已經初始化好了,咱們直接關注虛擬機執行字節碼的流程便可。

相關文章
相關標籤/搜索