Python interpreter
Author: 堇姬Naup
定義
先定義一下何謂python interpreter,就是用來解釋code object的
tiny interpreter
首先要建立一個解釋器,需要給他一個指令集
這邊先要求實現一件是,就是做加法
需要有以下指令
- LOAD_VALUE -> 載入數字至stack上、參數是要載入到stack上的數字
- ADD_TWO_VALUES -> 將stack最上層跟最上層-1 pop然後相加,將結果放到stack上、無參數
- PRINT_ANSWER -> 印出stack最上方層並pop掉、無參數
先將要加的兩個數字載入至stack上,相加後印出
並且加入
run_code()
循環執行每條指令及參數
1 | what_to_execute = { |
透過這樣來實現加法
變數
接下來引入一些簡單的mmap關係
LOAD_NAME -> 將變數值放到stack上
STORE_NAME -> 將變數mmap到stack最上層的值並pop
除了const表以外,還需要加入變數表
並新增environment來建立mmap關係
1 | what_to_execute = { |
優化run_code
這邊可以觀察到,使用if分支來建立指令集已經開始變得冗長,這邊我們可以使用getattr來優化動態查找
可以參考這篇
https://www.runoob.com/python/python-func-getattr.html
1 | def run_code(self, what_to_execute): |
Real Python Bytecode
以上簡單的了解了指令集的建立及該如何跑他,現在回歸原本的python bytecode來看看
1 | def f(): |
可以用__code__看一下
code
1 | def f(): |
觀察一下它裡面到底存了甚麼資訊
1 | co_argcount: 0 |
包含了許多函數相關bytecode以及執行資訊
.co_code下的就是他的bytecode
1 | b'd\x01}\x00|\x00d\x02k\x00r\x08d\x03S\x00d\x04S\x00' |
1 | [100, 1, 125, 0, 124, 0, 100, 2, 107, 0, 114, 8, 100, 3, 83, 0, 100, 4, 83, 0] |
這邊搭配dis來看
1 | 3 0 LOAD_CONST 1 (3) |
一個bytecode對應一個指令
1 | for i in list(f.__code__.co_code): |
對應關係
1 | 100 = LOAD_CONST |
LOAD_CONST 就相當於上方的 LOAD_FAST
LOAD_VALUE 就相當於上方的 LOAD_NAME
比較和循環
通常一個程式當然不可能只有單純的一行行執行,一定有條件判斷跟迴圈,而python bytecode也有類似的goto可以做跳轉
1 | if x < 5 |
被編譯成了
1 | 4 4 LOAD_FAST 0 (x) |
如果是false就跳到16行的no
對的話往下執行
POP_JUMP_IF_FALSE -> 將stack頂部的值pop掉看是true or false
1 | def f(): |
迴圈也是一樣的概念
1 | 3 0 LOAD_CONST 1 (1) |
看有沒有達成compare條件,並進行跳轉
Frame
接下來要介紹的是Frame,觀察一下可以發現 RETURN_VALUE會return一個值,那他究竟return到哪裡呢?
可以想像,一個bytecode object有很多的Frame,return的value會被return到main frame,可以簡單想像他是作用域的概念
如果一個遞迴函式調用自己十次,會有十一個frame
1 | def bar(y): |
現在共有三個frame,一但bar return就會把bar frame 從call stack pop掉
由於python在frame被pop掉幾乎都會清掉data stack,所以其實所有frame共用一個data stack也是可以的(非生成器的情況)
- 生成器的特殊性: 生成器的特性在於它可以暫停一個frame的執行,並在之後恢復到相同的狀態繼續執行,這要求每個frame必須有獨立的data stack
https://www.runoob.com/python3/python3-iterator-generator.html
Byterun
上述就是基礎的python interpreter的知識,開始看byterun吧
byterun有四種對象
- VirtualMachine
- 負責整體結構管理(frame call stack、指令到操作的映射)
- 處理指令到操作的映射
- 較複雜的管理邏輯
- Frame
- 關聯 code object
- 管理全局、局部命名空間
- 跟踪call stack和執行狀態
- Function
- 調用函數時,會創建一個新的 Frame
- 控制新 Frame 的創建和管理
- Block
https://github.com/nedbat/byterun
VirtualMachine
python interpreter就是一個VirtualMachine
VirtualMachine 負責保存call stack、異常狀態,以及在 frame 中傳遞的返回值
編譯後的code object為參數,並且創建一個frame,這個frame可能會在增加或刪減更多的frame,call stack也隨之伸縮,當第一個frame return時就結束(跟之前學的幾乎一樣)
1 | class VirtualMachine(object): |
frame
就是一個屬性的集合,包含了code object、global name、local name、內建命名空間、前一個frame、data stack、block stack
https://docs.python.org/zh-tw/3/library/builtins.html
https://www.cnblogs.com/goldsunshine/p/15085333.html
1 | class Frame(object): |
剛剛的virtual machine並沒有實現對於frame的操作,這邊加入
1 | class VirtualMachine(object): |
Function
重要的只有創建一個frame並執行他的__call__
1 | class Function(object): |
virtual machine也加入了管理stack更多方法
1 | class VirtualMachine(object): |
並且要加入解析指令是否有參數跟更新最後一行執行
1 | class VirtualMachine(object): |
block(沒有很重要,看看就好了)
block在控制流程中扮演重要角色,尤其是在處理異常和迴圈時。它確保在操作完成後,數據堆疊(data stack)的狀態是正確
在迴圈中,一個特殊的迭代器會存在於堆疊中,當迴圈完成時,它會從堆疊中彈出。解釋器需要檢查迴圈是否仍在進行中,或者已經停止
為了跟蹤這些額外的信息,解釋器設置了一個標誌來指示它的狀態。我們用一個變量 why 來實現這個標誌,它可以是 None 或以下幾個字符串之一:”continue”、”break”、”exception”、”return”。這些標誌指示如何對區塊堆疊和數據堆疊進行操作。例如,在迴圈的例子中,如果區塊堆疊的堆疊頂部是 loop 區塊,並且 why 是 “continue”,那麼迭代器應該保留在數據堆疊上;如果 why 是 “break”,那麼迭代器會被彈出。
(原文解釋得很好了)
1 | import collections |
實現指令集
基本上跟上面都差不多
1 | class VirtualMachine(object): |
詳細實現參考
https://github.com/nedbat/byterun
ref
https://bbs.kanxue.com/thread-258353.htm#stack