4834 words
24 minutes
Python Interpreter
2025-05-21

Python Interpreter#

專有名詞解釋#

這邊先列一些專有名詞

Code Object#

  • 意思是說經過 complied 的 python source code

AST 抽象語法樹#

  • 表示程式語言語法結構的樹狀資料結構
  • 只保留語意上的重要結構
  • 每個節點代表一種元素(變數、條件、迴圈等)

以 python 來說

a= 1+2

對應的 AST 會長這個樣子

Assign
├── Name(id='a')
└── BinOp
    ├── Num(n=1)
    ├── Add()
    └── Num(n=2)

Run python#

今天執行一個 python 的 code 的時候,他的流程大概會長這樣

  • Interpreter 會把傳進來的 Python Source code file.py 轉進來變成 Byte Code
  • 然後經過 interpret 之後的 Byte Code 才會被 Virtual Machine Complie 成 Machine Code 跟 Execute code 等
  • 最後 Virtual Machine 會 Call CPU ,經過一系列處理之後會變成我們程式碼想做的事

而我們通常用的都是 Cpython (以C為底所寫的python),所以下面的 Virtual Machine 我們都已 Cpython VM 去解釋

image

Tiny Interpreter#

我們今天有一個很小的直譯器,並且只能理解三個指令,然後要做一個求兩數和的東西,下面是指令

  • LOAD_Value 告訴解釋器把一個數字壓入 Stack 中,需要參數才能把指定數字
  • ADD_TWO_VALUES 將兩個值相加,他直譯器的 Stack 上 pop 所有要用的值,沒有參數
  • PRINT_ANSWER 列印 Stack 最上面,並把他 pop 掉

我們不用關心語法、編譯錯誤、還有指令怎麼產生的,我們單純就去寫 7+5

我們可以生出這樣的指令集

#7+5

what_to_execute = {
    "instructions": [("LOAD_VALUE", 0),  # the first number
                     ("LOAD_VALUE", 1),  # the second number
                     ("ADD_TWO_VALUES", None),
                     ("PRINT_ANSWER", None)],
    "numbers": [7, 5] }

Python 的直譯器是一個 Stack Machine,所以我們要用 Stack 的方式去跑

image

現在我們就先定義前面三個指令吧

class Interpreter:
    def __init__(self):
        self.stack = []

    def LOAD_VALUE(self, number):
        self.stack.append(number)

    def PRINT_ANSWER(self):
        answer = self.stack.pop()
        print(answer)

    def ADD_TWO_VALUES(self):
        first_num = self.stack.pop()
        second_num = self.stack.pop()
        total = first_num + second_num
        self.stack.append(total)

寫出了這些指令後,直譯器還需要一個能把所有東西結合在一起然後執行的方法 => run_code,她們我們前面定義的 what-to-execute 作為參數,把對應的方法填上去

    def run_code(self, what_to_execute):
        instructions = what_to_execute["instructions"]
        numbers = what_to_execute["numbers"]
        for each_step in instructions:
            instruction, argument = each_step
            if instruction == "LOAD_VALUE":
                number = numbers[argument]
                self.LOAD_VALUE(number)
            elif instruction == "ADD_TWO_VALUES":
                self.ADD_TWO_VALUES()
            elif instruction == "PRINT_ANSWER":
                self.PRINT_ANSWER()

然後再幫他開個物件

完整版:

what_to_execute = {
    "instructions": [("LOAD_VALUE", 0),  # the first number
                     ("LOAD_VALUE", 1),  # the second number
                     ("ADD_TWO_VALUES", None),
                     ("PRINT_ANSWER", None)],
    "numbers": [7, 5] }

class Interpreter:
    def __init__(self):
        self.stack = []

    def LOAD_VALUE(self, number):
        self.stack.append(number)

    def PRINT_ANSWER(self):
        answer = self.stack.pop()
        print(answer)

    def ADD_TWO_VALUES(self):
        first_num = self.stack.pop()
        second_num = self.stack.pop()
        total = first_num + second_num
        self.stack.append(total)
    def run_code(self, what_to_execute):
        instructions = what_to_execute["instructions"]
        numbers = what_to_execute["numbers"]
        for each_step in instructions:
            instruction, argument = each_step
            if instruction == "LOAD_VALUE":
                number = numbers[argument]
                self.LOAD_VALUE(number)
            elif instruction == "ADD_TWO_VALUES":
                self.ADD_TWO_VALUES()
            elif instruction == "PRINT_ANSWER":
                self.PRINT_ANSWER()
    interpreter = Interpreter()
    interpreter.run_code(what_to_execute)

Variables#

為了要讓直譯器多了變數的功能,我們又新增了一條指令

  • STORE_NAME 載入變數數值,需要參數
  • LOAD_NAME 載入變數名稱,需要參數

為了要追蹤變數名稱對應哪個值,我們新增了一個字典 environment,然後會發現指令參數可能是 numbers 的索引,也有可能是 names 的索引,直譯器可以透過直接執行來判斷是哪種參數,但我們今天把指令和參數 mmap 放在另一個方法中

    what_to_execute = {
        "instructions": [("LOAD_VALUE", 0),
                         ("STORE_NAME", 0),
                         ("LOAD_VALUE", 1),
                         ("STORE_NAME", 1),
                         ("LOAD_NAME", 0),
                         ("LOAD_NAME", 1),
                         ("ADD_TWO_VALUES", None),
                         ("PRINT_ANSWER", None)],
        "numbers": [1, 2],
        "names":   ["a", "b"] }
class Interpreter:
    def __init__(self):
        self.stack = []
        self.environment = {}

    def STORE_NAME(self, name):
        val = self.stack.pop()
        self.environment[name] = val

    def LOAD_NAME(self, name):
        val = self.environment[name]
        self.stack.append(val)

    def parse_argument(self, instruction, argument, what_to_execute):
        """ Understand what the argument to each instruction means."""
        numbers = ["LOAD_VALUE"]
        names = ["LOAD_NAME", "STORE_NAME"]

        if instruction in numbers:
            argument = what_to_execute["numbers"][argument]
        elif instruction in names:
            argument = what_to_execute["names"][argument]

        return argument

    def run_code(self, what_to_execute):
        instructions = what_to_execute["instructions"]
        for each_step in instructions:
            instruction, argument = each_step
            argument = self.parse_argument(instruction, argument, what_to_execute)

            if instruction == "LOAD_VALUE":
                self.LOAD_VALUE(argument)
            elif instruction == "ADD_TWO_VALUES":
                self.ADD_TWO_VALUES()
            elif instruction == "PRINT_ANSWER":
                self.PRINT_ANSWER()
            elif instruction == "STORE_NAME":
                self.STORE_NAME(argument)
            elif instruction == "LOAD_NAME":
                self.LOAD_NAME(argument)
interpreter = Interpreter()
interpreter.run_code(what_to_execute)

優化 run_code#

可以看到 run_code 已經變得很冗長了,所以這邊我們用了 python 的動態方法去查找,這樣就可以用 python 的 getattr 去找ㄌ

    def execute(self, what_to_execute):
        instructions = what_to_execute["instructions"]
        for each_step in instructions:
            instruction, argument = each_step
            argument = self.parse_argument(instruction, argument, what_to_execute)
            bytecode_method = getattr(self, instruction)
            if argument is None:
                bytecode_method()
            else:
                bytecode_method(argument)

Real Python ByteCode#

Byte Code 是 Python Complier 把 AST Complie 之後給 Cpython VM 所執行的東西,而顧名思義就是以 Byte 為主的內容,正常情況下我們是看不懂他在幹嘛的但是今天我們可以用 python 內部的 dis 去看一隻 python 程式的 Byte Code 具體會長怎樣

流程圖:
原始碼 => AST => Bytecode => 執行(在 CPython VM中)

可以用 cond 這個函數物件去看跟他有關的 code object,而cond.__code__.co_code 是他的 byte code,一般情況下會長這樣

>>> cond.__code__.co_code  # the bytecode as raw bytes
b'd\x01\x00}\x00\x00|\x00\x00d\x02\x00k\x00\x00r\x16\x00d\x03\x00Sd\x04\x00Sd\x00
   \x00S'
>>> list(cond.__code__.co_code)  # the bytecode as numbers
[100, 1, 0, 125, 0, 0, 124, 0, 0, 100, 2, 0, 107, 0, 0, 114, 22, 0, 100, 3, 0, 83, 
 100, 4, 0, 83, 100, 0, 0, 83]

這時候我們完全看不出這些 bytecode 在幹嘛的,

所以我們可以用 dis 去看這些 byte code 在幹嘛

這邊隨便寫一個會輸出 Meow 三次的程式

import dis

def test():
    for i in range(3):
        print('meow meow meow')

dis.dis(test)

執行之後他的 Byte Code 會長這樣

  4           0 LOAD_GLOBAL              0 (range)
              2 LOAD_CONST               1 (3)
              4 CALL_FUNCTION            1
              6 GET_ITER
        >>    8 FOR_ITER                12 (to 22)
             10 STORE_FAST               0 (i)

  5          12 LOAD_GLOBAL              1 (print)
             14 LOAD_CONST               2 ('meow meow meow')
             16 CALL_FUNCTION            1
             18 POP_TOP
             20 JUMP_ABSOLUTE            8
        >>   22 LOAD_CONST               0 (None)
             24 RETURN_VALUE

從這些 Byte Code 推回去的話是先從 globals 找到 range 這個 function 然後 LOAD_GLOBAL 是用來載入 rangeprint 這些全域定義,之後 LOAD_CONST 看迴圈要執行幾次,知道幾次之後再 CALL_FUNCTION 呼叫函數,表示有 1 個參數 (3),會變成 range(3)

GET_ITER FOR_ITER 就是把這些東西轉換成可以被 for loop 所使用的東西而已

STORE_FAST 代表把剛剛的 ITER 存到區域變數 i

之後就是用 LOAD_GLOBAL CALL print 跟載入我們給的值 Meow Meow Meow 接著在 CALL 這個 function

這邊POP_TOP意思是說 print return 0 ,所以把他從 STACK 上丟掉 JUMP_ABSOLUTE代表跑回去一開始的迴圈 最後的 LOAD_CONST 代表迴圈結束後預設的 return value 是 0,然後把 value Return 回來

rangeprint etc.. 這些 function 都是 built_in function,所以都隸屬於 globals

我們如果要看 byte code 中數字代表指令的意思,也可以用 dis.opname[?] 去看

>>> dis.opname[100]
'LOAD_CONST'
>>> dis.opname[125]
'STORE_FAST'

比較和循環#

程式中一定會有迴圈跟判斷條件,python 的 byte code 也有類似 goto 的東西可以做跳轉

if x < 5

會變成

4           4 LOAD_FAST                0 (x)
            6 LOAD_CONST               2 (5)
            8 COMPARE_OP               0 (<)
           10 POP_JUMP_IF_FALSE        8 (to 16)

如果是 False 就跳到第 16 行

然後 while 跟前面 for 不太一樣,在這邊會跟上面比較相近

def f():
    x = 1
    while x < 5:
        x = x + 1
    return x

=>

3           0 LOAD_CONST               1 (1)
            2 STORE_FAST               0 (x)

4           4 LOAD_FAST                0 (x)
            6 LOAD_CONST               2 (5)
            8 COMPARE_OP               0 (<)
           10 POP_JUMP_IF_FALSE       14 (to 28)

5     >>   12 LOAD_FAST                0 (x)
           14 LOAD_CONST               1 (1)
           16 BINARY_ADD
           18 STORE_FAST               0 (x)

4          20 LOAD_FAST                0 (x)
           22 LOAD_CONST               2 (5)
           24 COMPARE_OP               0 (<)
           26 POP_JUMP_IF_TRUE         6 (to 12)

6     >>   28 LOAD_FAST                0 (x)
           30 RETURN_VALUE

Frames#

我們知道 python VM 是一個 Stack Machine,在 Stack 上做一些操作,那最後的 Return Value 到底去了哪呢?

要回答前面那個問題,我們要先知道 frame 這個東西

首先 Frame 是執行每一個 function 時,所創建的結構,他存了很多 function 執行的資料,一個 code object 中會有很多 frame,可以把它想像成一個小包廂,每一次 call function 的時候,他就會創造一個新的 frame 來執行所需的東西

然後 frame 上的東西有

  • 這個函式有哪些變數(local variables)

  • 下一行要執行什麼(程式計數器)

  • 上層呼叫者是誰(上一層 frame)

假設我今天 call 自己 10 次,這時會有 11 個 frame

def bar(y):
    x=y+3
    return x

def foo():
    a=1
    b=2
    return a + bar(b)

foo()

現在有三個 frame,當 bar return ,跟他對應的 frame 就會從 Stack 上 pop 掉

image

回到前面 Byte Instruction 的 RETURN_VALUE 告訴直譯器 frame 間傳遞一個值,他會先把 Stack 最上面的 frame 數據給丟出,然後把整個 frame 丟掉,最後把剛剛最上面的那個數據壓入 Stack 上

ByteRun#

上面都是 python Interpreter 的基礎,往下來看 ByteRun ㄅ

ByteRun 有種對象

  • VirtualMachine 類
    • 管理高層結構,frame 調用 Stack,指令到操作的 mmap,比前面 Inteprter 物件更複雜的版本
  • Frame 類
    • 每個 Frame 類都有一個 code object ,管理其他一些必要的狀態資訊,像是全域和區域命名空間,指向調用他的 frame pointer 和最後執行的 Byte 指令
  • Function 類
    • call function 時創造一個新的 frame
    • 管理和控制新的 frame
  • Block 類
    • 包中程式碼區塊的三個屬性 (這邊不會用到)

The VirtualMachine Class#

VirtualMachine 就是用來執行 byte code 的 VM

VirtualMachine 保存呼叫 Stack,例外狀態,在 frame 中傳遞回傳值

他的進入點是方法 run_code,他在編譯後的 code object 為參數,以建立一個 frame 為開始,然後執行這個 frame,這個 frame 可能會在創建新的 frame,當第一個 frame 回傳時,執行結束

class VirtualMachineError(Exception):
    pass

class VirtualMachine(object):
    def __init__(self):
        self.frames = []   # The call stack of frames.
        self.frame = None  # The current frame.
        self.return_value = None
        self.last_exception = None

    def run_code(self, code, global_names=None, local_names=None):
        """ An entry point to execute code using the virtual machine."""
        frame = self.make_frame(code, global_names=global_names, 
                                local_names=local_names)
        self.run_frame(frame)

frames#

他是一個屬性的集合,沒有任何方法,他的屬性包含了 code object、區域、全域變數,還有命名空間等

前一個 frame 的引用,一個資料 Stack,一個 Block Stack,

class Frame(object):
    def __init__(self, code_obj, global_names, local_names, prev_frame):
        self.code_obj = code_obj
        self.global_names = global_names
        self.local_names = local_names
        self.prev_frame = prev_frame
        self.stack = []
        if prev_frame:
            self.builtin_names = prev_frame.builtin_names
        else:
            self.builtin_names = local_names['__builtins__']
            if hasattr(self.builtin_names, '__dict__'):
                self.builtin_names = self.builtin_names.__dict__

        self.last_instruction = 0
        self.block_stack = []

前面只宣告了 Frame 上的東西,現在我們要寫他的操作方式,包含了

  • 創建新的 frame
  • push 去 Stack 的方法
  • pop 出 Stack 的方法
  • 還有運行 frame (run_frame)
class VirtualMachine(object):
    [... snip ...]

    # Frame manipulation
    def make_frame(self, code, callargs={}, global_names=None, local_names=None):
        if global_names is not None and local_names is not None:
            local_names = global_names
        elif self.frames:
            global_names = self.frame.global_names
            local_names = {}
        else:
            global_names = local_names = {
                '__builtins__': __builtins__,
                '__name__': '__main__',
                '__doc__': None,
                '__package__': None,
            }
        local_names.update(callargs)
        frame = Frame(code, global_names, local_names, self.frame)
        return frame

    def push_frame(self, frame):
        self.frames.append(frame)
        self.frame = frame

    def pop_frame(self):
        self.frames.pop()
        if self.frames:
            self.frame = self.frames[-1]
        else:
            self.frame = None

    def run_frame(self):
        pass
        # we'll come back to this shortly

Function#

Funtion 在直譯器中的細節不是很重要,只有在__call__這個方法被調用時,才會創建一個新的 Frame 並執行他

class Function(object):
    """
    Create a realistic function object, defining the things the interpreter expects.
    """
    __slots__ = [
        'func_code', 'func_name', 'func_defaults', 'func_globals',
        'func_locals', 'func_dict', 'func_closure',
        '__name__', '__dict__', '__doc__',
        '_vm', '_func',
    ]

    def __init__(self, name, code, globs, defaults, closure, vm):
        """You don't need to follow this closely to understand the interpreter."""
        self._vm = vm
        self.func_code = code
        self.func_name = self.__name__ = name or code.co_name
        self.func_defaults = tuple(defaults)
        self.func_globals = globs
        self.func_locals = self._vm.frame.f_locals
        self.__dict__ = {}
        self.func_closure = closure
        self.__doc__ = code.co_consts[0] if code.co_consts else None

        # Sometimes, we need a real Python function.  This is for that.
        kw = {
            'argdefs': self.func_defaults,
        }
        if closure:
            kw['closure'] = tuple(make_cell(0) for _ in closure)
        self._func = types.FunctionType(code, globs, **kw)

    def __call__(self, *args, **kwargs):
        """When calling a Function, make a new frame and run it."""
        callargs = inspect.getcallargs(self._func, *args, **kwargs)
        # Use callargs to provide a mapping of arguments: values to pass into the new 
        # frame.
        frame = self._vm.make_frame(
            self.func_code, callargs, self.func_globals, {}
        )
        return self._vm.run_frame(frame)

def make_cell(value):
    """Create a real Python closure and grab a cell."""
    # Thanks to Alex Gaynor for help with this bit of twistiness.
    fn = (lambda x: lambda: x)(value)
    return fn.__closure__[0]

對資料 Stack 上也增加方法,Byte Code 操作的 Stack 總是在當前的 Frame Stack

class VirtualMachine(object):
    [... snip ...]

    # Data stack manipulation
    def top(self):
        return self.frame.stack[-1]

    def pop(self):
        return self.frame.stack.pop()

    def push(self, *vals):
        self.frame.stack.extend(vals)

    def popn(self, n):
        """Pop a number of values from the value stack.
        A list of `n` values is returned, the deepest value first.
        """
        if n:
            ret = self.frame.stack[-n:]
            self.frame.stack[-n:] = []
            return ret
        else:
            return []

在運行 frame 之前,我們還需要兩個方法

第一個方法,用 parse_byte_and_args 來看說他是否有參數,如果有,就 parse 他的參數

def parse_byte_and_args(self):
    f = self.frame  # 取得目前執行中的 frame(也就是當前的執行環境)
    opoffset = f.last_instruction  # 當前 bytecode 的偏移位置
    byteCode = f.code_obj.co_code[opoffset]  # 取得該位置的 bytecode(opcode 數值)
    f.last_instruction += 1  # 移動到下一個 bytecode 位置
    byte_name = dis.opname[byteCode]  # 取得該 bytecode 對應的名稱,例如 LOAD_CONST

    if byteCode >= dis.HAVE_ARGUMENT:
        # 有額外參數的指令(參數為兩個 byte 構成,低位在前,高位在後)
        arg = f.code_obj.co_code[f.last_instruction:f.last_instruction+2]  # 抓取 2 個 byte 的參數
        f.last_instruction += 2  # 向前推進 2 byte

        # 將兩個 byte 合成一個整數值(little-endian)
        arg_val = arg[0] + (arg[1] * 256)

        # 根據指令類型,把參數值轉換成對應的內容
        if byteCode in dis.hasconst:
            # 若是常數相關指令(如 LOAD_CONST),從 co_consts 取出實際值
            arg = f.code_obj.co_consts[arg_val]
        elif byteCode in dis.hasname:
            # 若是變數名稱相關指令(如 LOAD_NAME),從 co_names 取出變數名稱
            arg = f.code_obj.co_names[arg_val]
        elif byteCode in dis.haslocal:
            # 若是區域變數(如 LOAD_FAST),從 co_varnames 取出變數名稱
            arg = f.code_obj.co_varnames[arg_val]
        elif byteCode in dis.hasjrel:
            # 若是跳躍指令(如 JUMP_FORWARD),計算跳轉後的位移
            arg = f.last_instruction + arg_val
        else:
            # 其他指令,參數就保留為整數值
            arg = arg_val

        # 把參數包裝成 list 回傳(因為 dispatch 接受 list 作為 *args)
        argument = [arg]
    else:
        # 沒有額外參數的 bytecode(例如 RETURN_VALUE)
        argument = []

    return byte_name, argument  # 回傳指令名稱和對應參數
t

第二個方法是用 dispathch , 用 getattr 來找,如果一條指令叫做 FOO_BAR,那他對應的方法就是 byte_FOO_BAR,然後每個指令都有 return None 或加上一個 String => why

class VirtualMachine(object):
    [...]

    def dispatch(self, byte_name, argument):
        """根據 bytecode 指令名稱分派對應的方法來執行。
        如果指令不支援,則拋出錯誤;如果執行時發生例外,也會記錄。"""

        # 我們需要追蹤執行結束的原因,例如是正常 return、跳出 loop 還是 exception。
        why = None
        try:
            # 根據 byte_name 找到對應的方法(例如 byte_LOAD_CONST)。
            bytecode_fn = getattr(self, 'byte_%s' % byte_name, None)

            if bytecode_fn is None:
                # 如果不是已定義的指令函式,試著以 UNARY_ 或 BINARY_ 處理
                if byte_name.startswith('UNARY_'):
                    self.unaryOperator(byte_name[6:])  # 處理一元運算,例如 UNARY_NEGATIVE
                elif byte_name.startswith('BINARY_'):
                    self.binaryOperator(byte_name[7:])  # 處理二元運算,例如 BINARY_ADD
                else:
                    # 若不符合任何可處理的 bytecode,拋出錯誤
                    raise VirtualMachineError(
                        "unsupported bytecode type: %s" % byte_name
                    )
            else:
                # 執行對應的 bytecode 方法,可能會回傳為何要中斷執行(例如 'return' 或 'exception')
                why = bytecode_fn(*argument)
        except:
            # 如果在執行指令時發生例外,記錄錯誤並設為中止原因
            self.last_exception = sys.exc_info()[:2] + (None,)
            why = 'exception'

        return why

    def run_frame(self, frame):
        """執行一個 frame(等同一個函式呼叫範圍)直到它結束為止。
        如果中途發生例外就拋出,否則回傳執行結果。"""

        # 把這個 frame 加入執行堆疊中
        self.push_frame(frame)

        while True:
            # 從 bytecode 中解析出目前要執行的指令與參數
            byte_name, arguments = self.parse_byte_and_args()

            # 執行該 bytecode 指令,取得為何要結束執行(或 None)
            why = self.dispatch(byte_name, arguments)

            # 如果有中斷原因,並且目前有 block(如 try-finally, for, while 等),就處理 block stack
            while why and frame.block_stack:
                why = self.manage_block_stack(why)

            # 如果已經有明確的結束原因(如 return 或 exception),就跳出迴圈
            if why:
                break

        # 執行完後,把這個 frame 從堆疊中移除
        self.pop_frame()

        # 如果結束原因是例外,就重新拋出例外(轉為 Python 的例外)
        if why == 'exception':
            exc, val, tb = self.last_exception
            e = exc(val)
            e.__traceback__ = tb
            raise e

        # 否則就回傳執行結果(由 RETURN_VALUE 設定)
        return self.return_value

Block (在這邊沒啥重要ㄉ)#

Block 負責保障迴圈或異常廚李後的資料 Stack 是不是處於正確狀態

為了追蹤這些額外的信息,解釋器設置了一個 flag 來指示它的狀態。我們使用一個變數 why 實現這個標誌,它可以 None 或者是下面幾個字串這一, “continue” , “break” , “excption” , return 。他們指示對塊栈和資料栈進行什麼操作。回到我們迭代器的例子,如果塊栈的栈頂是一個 loop 塊, why 是 continue ,迭代器就應該保存在資料栈上,不是如果 why 是 break ,迭代器就會被彈出。

Block = collections.namedtuple("Block", "type, handler, stack_height")

class VirtualMachine(object):
    [... snip ...]

    # Block stack manipulation
    def push_block(self, b_type, handler=None):
        level = len(self.frame.stack)
        self.frame.block_stack.append(Block(b_type, handler, stack_height))

    def pop_block(self):
        return self.frame.block_stack.pop()

    def unwind_block(self, block):
        """Unwind the values on the data stack corresponding to a given block."""
        if block.type == 'except-handler':
            # The exception itself is on the stack as type, value, and traceback.
            offset = 3  
        else:
            offset = 0

        while len(self.frame.stack) > block.level + offset:
            self.pop()

        if block.type == 'except-handler':
            traceback, value, exctype = self.popn(3)
            self.last_exception = exctype, value, traceback

    def manage_block_stack(self, why):
        """ """
        frame = self.frame
        block = frame.block_stack[-1]
        if block.type == 'loop' and why == 'continue':
            self.jump(self.return_value)
            why = None
            return why

        self.pop_block()
        self.unwind_block(block)

        if block.type == 'loop' and why == 'break':
            why = None
            self.jump(block.handler)
            return why

        if (block.type in ['setup-except', 'finally'] and why == 'exception'):
            self.push_block('except-handler')
            exctype, value, tb = self.last_exception
            self.push(tb, value, exctype)
            self.push(tb, value, exctype) # yes, twice
            why = None
            self.jump(block.handler)
            return why

        elif block.type == 'finally':
            if why in ('return', 'continue'):
                self.push(self.return_value)

            self.push(why)

            why = None
            self.jump(block.handler)
            return why
        return why

Instructions#

剩下就是指令了,像是byte_LOAD_FAST , byte_BINARY_MODULO 等等,這邊只有一小段

class VirtualMachine(object):
    [... snip ...]

    ## Stack manipulation

    def byte_LOAD_CONST(self, const):
        self.push(const)

    def byte_POP_TOP(self):
        self.pop()

    ## Names
    def byte_LOAD_NAME(self, name):
        frame = self.frame
        if name in frame.f_locals:
            val = frame.f_locals[name]
        elif name in frame.f_globals:
            val = frame.f_globals[name]
        elif name in frame.f_builtins:
            val = frame.f_builtins[name]
        else:
            raise NameError("name '%s' is not defined" % name)
        self.push(val)

    def byte_STORE_NAME(self, name):
        self.frame.f_locals[name] = self.pop()

    def byte_LOAD_FAST(self, name):
        if name in self.frame.f_locals:
            val = self.frame.f_locals[name]
        else:
            raise UnboundLocalError(
                "local variable '%s' referenced before assignment" % name
            )
        self.push(val)

    def byte_STORE_FAST(self, name):
        self.frame.f_locals[name] = self.pop()

    def byte_LOAD_GLOBAL(self, name):
        f = self.frame
        if name in f.f_globals:
            val = f.f_globals[name]
        elif name in f.f_builtins:
            val = f.f_builtins[name]
        else:
            raise NameError("global name '%s' is not defined" % name)
        self.push(val)

    ## Operators

    BINARY_OPERATORS = {
        'POWER':    pow,
        'MULTIPLY': operator.mul,
        'FLOOR_DIVIDE': operator.floordiv,
        'TRUE_DIVIDE':  operator.truediv,
        'MODULO':   operator.mod,
        'ADD':      operator.add,
        'SUBTRACT': operator.sub,
        'SUBSCR':   operator.getitem,
        'LSHIFT':   operator.lshift,
        'RSHIFT':   operator.rshift,
        'AND':      operator.and_,
        'XOR':      operator.xor,
        'OR':       operator.or_,
    }

    def binaryOperator(self, op):
        x, y = self.popn(2)
        self.push(self.BINARY_OPERATORS[op](x, y))

    COMPARE_OPERATORS = [
        operator.lt,
        operator.le,
        operator.eq,
        operator.ne,
        operator.gt,
        operator.ge,
        lambda x, y: x in y,
        lambda x, y: x not in y,
        lambda x, y: x is y,
        lambda x, y: x is not y,
        lambda x, y: issubclass(x, Exception) and issubclass(x, y),
    ]

    def byte_COMPARE_OP(self, opnum):
        x, y = self.popn(2)
        self.push(self.COMPARE_OPERATORS[opnum](x, y))

    ## Attributes and indexing

    def byte_LOAD_ATTR(self, attr):
        obj = self.pop()
        val = getattr(obj, attr)
        self.push(val)

    def byte_STORE_ATTR(self, name):
        val, obj = self.popn(2)
        setattr(obj, name, val)

    ## Building

    def byte_BUILD_LIST(self, count):
        elts = self.popn(count)
        self.push(elts)

    def byte_BUILD_MAP(self, size):
        self.push({})

    def byte_STORE_MAP(self):
        the_map, val, key = self.popn(3)
        the_map[key] = val
        self.push(the_map)

    def byte_LIST_APPEND(self, count):
        val = self.pop()
        the_list = self.frame.stack[-count] # peek
        the_list.append(val)

    ## Jumps

    def byte_JUMP_FORWARD(self, jump):
        self.jump(jump)

    def byte_JUMP_ABSOLUTE(self, jump):
        self.jump(jump)

    def byte_POP_JUMP_IF_TRUE(self, jump):
        val = self.pop()
        if val:
            self.jump(jump)

    def byte_POP_JUMP_IF_FALSE(self, jump):
        val = self.pop()
        if not val:
            self.jump(jump)

    ## Blocks

    def byte_SETUP_LOOP(self, dest):
        self.push_block('loop', dest)

    def byte_GET_ITER(self):
        self.push(iter(self.pop()))

    def byte_FOR_ITER(self, jump):
        iterobj = self.top()
        try:
            v = next(iterobj)
            self.push(v)
        except StopIteration:
            self.pop()
            self.jump(jump)

    def byte_BREAK_LOOP(self):
        return 'break'

    def byte_POP_BLOCK(self):
        self.pop_block()

    ## Functions

    def byte_MAKE_FUNCTION(self, argc):
        name = self.pop()
        code = self.pop()
        defaults = self.popn(argc)
        globs = self.frame.f_globals
        fn = Function(name, code, globs, defaults, None, self)
        self.push(fn)

    def byte_CALL_FUNCTION(self, arg):
        lenKw, lenPos = divmod(arg, 256) # KWargs not supported here
        posargs = self.popn(lenPos)

        func = self.pop()
        frame = self.frame
        retval = func(*posargs)
        self.push(retval)

    def byte_RETURN_VALUE(self):
        self.return_value = self.pop()
        return "return"

動態型別: Complier 不知道的內容#

Python 是一種動態語言,動態的意思是很多工作都在運行的時候完成,我們以下面的 mod 當例子,我們可以看到 Byte code 中,a 和 b 這兩個變數先被載入,最後 Byte Code VINAY_MODULO 完成這個運算

>>> def mod(a, b):
...    return a % b
>>> dis.dis(mod)
  2           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_MODULO
              7 RETURN_VALUE
>>> mod(19, 5)
4

可以計算出 19 % 5 = 4,那假設我們今天用不同類的參數呢?

>>> mod("by%sde", "teco")
'bytecode'

在這邊,我用 python 中 Format String 的語法去跑他

會發現都會產生 BINARY_MODULO,但是結果卻差很多,也可以看到,不管我傳是整數還是字串,Python 的 ByteCode 都是一樣的,具體原因是 Python 的 Complier 並不是決定 a%b 是啥意思,他只是負責產生 BINARY_MODULO 而已,真正的邏輯是交給 runtime

Python 直譯器根據物件類型,去決定如何執行 % =>

Python 解釋器在執行 BINARY_MODULO 時會做以下事:

  • 檢查 Stack top 兩個值的型別

  • 如果是 int 就執行數值模運算

  • 如果是 str 就執行格式化

  • 如果是自定義物件,就呼叫 obj.mod() 方法

Reference#

https://blog.louie.lu/2017/04/11/python-%E5%BA%95%E5%B1%A4%E9%81%8B%E4%BD%9C-01-%E8%99%9B%E6%93%AC%E6%A9%9F%E5%99%A8%E8%88%87-byte-code/ Python interpreter

https://qingyunha.github.io/taotao/

https://naupjjin.github.io/2024/08/22/Python-interpreter/

https://blog.codingconfessions.com/p/cpython-vm-internals

Python Interpreter
https://dkri3c1.github.io/posts/python-interpreter/
Author
dkri3c1
Published at
2025-05-21