深入理解 python 虛擬機器:位元組碼教程(1)——原來裝飾器是這樣實現的

一無是處的研究僧發表於2023-04-09

深入理解 python 虛擬機器:位元組碼教程(1)——原來裝飾器是這樣實現的

在本篇文章當中主要給大家介紹在 cpython 當中一些比較常見的位元組碼,從根本上理解 python 程式的執行。在本文當中主要介紹一些 python 基本操作的位元組碼,並且將從位元組碼的角度分析函式裝飾器的原理!

Python 常見位元組碼

LOAD_CONST

這個指令用於將一個常量載入到棧中。常量可以是數字、字串、元組、列表、字典等物件。例如:

>>> dis.dis(lambda: 42)
  1           0 LOAD_CONST               1 (42)
              2 RETURN_VALUE

LOAD_NAME

這個指令用於將一個變數載入到棧中。例如:

>>> dis.dis(lambda: x)
  1           0 LOAD_GLOBAL              0 (x)
              2 RETURN_VALUE
>>>

STORE_NAME

這個指令用於將棧頂的值儲存到一個變數中。例如:

>>> dis.dis("x=42")
  1           0 LOAD_CONST               0 (42)
              2 STORE_NAME               0 (x)
              4 LOAD_CONST               1 (None)
              6 RETURN_VALUE

BINARY_ADD

這個指令用於對棧頂的兩個值進行加法運算並將結果推送到棧中。

>>> dis.dis(lambda: x + y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_ADD
              6 RETURN_VALUE

BINARY_SUBTRACT

這個指令用於對棧頂的兩個值進行減法運算並將結果推送到棧中。

>>> dis.dis(lambda: x - y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_SUBTRACT
              6 RETURN_VALUE

同樣的加減乘除取餘數的位元組碼如下所示:

>>> dis.dis(lambda: x + y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_ADD
              6 RETURN_VALUE
>>> dis.dis(lambda: x - y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_SUBTRACT
              6 RETURN_VALUE
>>> dis.dis(lambda: x * y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_MULTIPLY
              6 RETURN_VALUE
>>> dis.dis(lambda: x / y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_TRUE_DIVIDE
              6 RETURN_VALUE
>>> dis.dis(lambda: x // y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_FLOOR_DIVIDE
              6 RETURN_VALUE
>>> dis.dis(lambda: x % y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_MODULO
              6 RETURN_VALUE

COMPARE_OP

這個指令用於比較棧頂的兩個值,並且將比較得到的結果壓入棧中,這個位元組碼後面後一個位元組的引數,表示小於大於不等於等等比較符號。例如:

>>> dis.dis(lambda: x - y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 BINARY_SUBTRACT
              6 RETURN_VALUE
>>> dis.dis(lambda: x > y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               4 (>)
              6 RETURN_VALUE
>>> dis.dis(lambda: x < y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               0 (<)
              6 RETURN_VALUE
>>> dis.dis(lambda: x != y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               3 (!=)
              6 RETURN_VALUE
>>> dis.dis(lambda: x <= y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               1 (<=)
              6 RETURN_VALUE
>>> dis.dis(lambda: x >= y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               5 (>=)
              6 RETURN_VALUE
>>> dis.dis(lambda: x == y)
  1           0 LOAD_GLOBAL              0 (x)
              2 LOAD_GLOBAL              1 (y)
              4 COMPARE_OP               2 (==)
              6 RETURN_VALUE

RETURN_VALUE

將棧頂元素彈出作為返回值。

BUILD_LIST

這個指令用於建立一個列表。例如:

>>> dis.dis(lambda: [a, b, c, e])
  1           0 LOAD_GLOBAL              0 (a)
              2 LOAD_GLOBAL              1 (b)
              4 LOAD_GLOBAL              2 (c)
              6 LOAD_GLOBAL              3 (e)
              8 BUILD_LIST               4
             10 RETURN_VALUE

這條位元組碼指令有一個參數列示棧空間當中列表元素的個數,在上面的例子當中這個引數是 4 。

BUILD_TUPLE

這個指令用於建立一個元組。例如:

>>> dis.dis(lambda: (a, b, c))
  1           0 LOAD_GLOBAL              0 (a)
              2 LOAD_GLOBAL              1 (b)
              4 LOAD_GLOBAL              2 (c)
              6 BUILD_TUPLE              3
              8 RETURN_VALUE

同樣的這個位元組碼也有一個引數,表示建立元組的元素個數。

BUILD_MAP

這個指令用於建立一個字典。例如:


BUILD_SET

和 list 和 tuple 一樣,這條指令是用於建立一個集合物件,同樣的這條指令也有一個參數列示用於建立集合的元素的個數。

>>> dis.dis(lambda: {a, b, c, d})
  1           0 LOAD_GLOBAL              0 (a)
              2 LOAD_GLOBAL              1 (b)
              4 LOAD_GLOBAL              2 (c)
              6 LOAD_GLOBAL              3 (d)
              8 BUILD_SET                4
             10 RETURN_VALUE

BUILD_CONST_KEY_MAP

這條指令是用於建立一個字典物件,同樣的這條指令也有一個引數,表示字典當中元素的個數。

>>> dis.dis(lambda: {1:2, 3:4})
  1           0 LOAD_CONST               1 (2)
              2 LOAD_CONST               2 (4)
              4 LOAD_CONST               3 ((1, 3))
              6 BUILD_CONST_KEY_MAP      2
              8 RETURN_VALUE

從位元組碼角度分析裝飾器的原理

如果你是一個 pythoner 那麼你肯定或多或少聽說過裝飾器,這是一個 python 的語法糖我們可以用它來做很多有趣的事情,比如在不修改原始碼的基礎之上給函式附加一些功能,比如說計算時間。

import time

def eval_time(func):
    
    def cal_time(*args, **kwargs):
        start = time.time()
        r = func(*args, **kwargs)
        end = time.time()
        return r, end - start
    return cal_time


@eval_time
def fib(n):
    a = 0
    b = 1
    while n > 0:
        n -= 1
        a, b = b, a + b
    return a

在上面的程式碼當中我們實現了一個計算斐波拉契數列的函式,除此之外還寫了一個 eval_time 函式用於計算函式執行的時間,現在呼叫函式 fib(10),程式的輸出如下所示:

>>>fib(10)
(55, 5.9604644775390625e-06)

可以看到實現了我們想要的效果。

現在我們使用一個更加簡單的例子來模擬上面的程式碼結構,方便我們對上面函式執行的過程進行分析:

s = """
def decorator(func):
    print("Hello")
    return func

@decorator
def fib(n):
    pass
"""
dis.dis(s)

上面的 dis 函式的輸出對應程式碼的位元組碼如下所示:

  2           0 LOAD_CONST               0 (<code object decorator at 0x108068d40, file "<dis>", line 2>)
              2 LOAD_CONST               1 ('decorator')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (decorator)

  6           8 LOAD_NAME                0 (decorator)

  7          10 LOAD_CONST               2 (<code object fib at 0x1075c1710, file "<dis>", line 6>)
             12 LOAD_CONST               3 ('fib')
             14 MAKE_FUNCTION            0
             16 CALL_FUNCTION            1
             18 STORE_NAME               1 (fib)
             20 LOAD_CONST               4 (None)
             22 RETURN_VALUE

Disassembly of <code object decorator at 0x108068d40, file "<dis>", line 2>:
  3           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('Hello')
              4 CALL_FUNCTION            1
              6 POP_TOP

  4           8 LOAD_FAST                0 (func)
             10 RETURN_VALUE

Disassembly of <code object fib at 0x1075c1710, file "<dis>", line 6>:
  8           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
  • 執行第一條指令 LOAD_CONST,這條指令主要是載入一個 code object 物件,這個物件裡面主要是包含函式 decorator 的位元組碼,主要是上面位元組碼的第二塊內容。在執行完這條位元組碼之後棧空間如下所示:

  • 執行完第二條指令 LOAD_CONST 之後,會將字串 decorator 載入進入棧空間當中。

  • 執行第三條指令 MAKE_FUNCTION,這條位元組碼的作用是在虛擬機器內部建立一個函式,函式的名稱為 decorator,函式對應的位元組碼則是在先前壓入棧空間當中的 code object 物件,這條指令還會將建立好的函式物件壓入棧中。

  • STORE_NAME,條位元組碼會將棧頂的元素彈出,並且將 co_names[oparg] 指向這個物件,在上面的位元組碼當中 co_names[oparg] 就是 decorator 。

  • LOAD_NAME,這條位元組碼就是將 co_names[oparg] 對應的名字指向的物件重新載入進入棧空間當中,也就是上面的 decorator 函式加入進行棧空間當中。

  • 接下來的三條位元組碼 LOAD_CONST,LOAD_CONST 和 MAKE_FUNCTION,在執行這三條位元組碼之後,棧空間如下所示:

  • 接下來的一條指令非常重要,這條指令便是裝飾器的核心原理,CALL_FUNCTION 這條指令有一個引數 i,在上面的位元組碼當中為 1,也就是說從棧頂開始的前 i 個元素都是函式引數,呼叫的函式在棧空間的位置為 i + 1 (從棧頂往下數),那麼在上面的情況下就是說呼叫 decorator 函式,並且將 fib 函式作為 decorator 函式的引數,decorator 函式的返回值再壓入棧頂。在上面的程式碼當中 decorator 函式返回值也是一個函式,也就是 decorator 函式的引數,即 fib 函式。

  • 接下來便是 STORE_NAME 位元組碼,這條位元組碼的含義我們在前面已經說過了,就是將棧頂元素彈出,儲存到 co_names[oparg] 指向的物件當中,在上面的程式碼當中也就是將棧頂的物件儲存到 fib 當中。棧頂元素 fib 函式是呼叫函式 decorator 的返回值。

看到這裡就能夠理解了原來裝飾器的最根本的原理不就是函式呼叫嘛,比如我們最前面的用於計算函式執行時間的裝飾器的原理就是:

fib = eval_time(fib)

將 fib 函式作為 eval_time 函式的引數,再將這個函式的返回值儲存到 fib 當中,當然這個物件必須是可呼叫的,不然後面使用 fib() 就會儲存,我們可以使用下面的程式碼來驗證這個效果。

def decorator(func):
    return func()


@decorator
def demo():
    return "function demo return string : Demo"

print(demo)

執行上面的程式結果為:

function demo return string : Demo

可以看到 demo 已經變成了一個字串物件而不再是一個函式了,因為 demo = decorator(demo),而在函式 decorator 當中返回值是 demo 函式自己的返回值,因此才列印了字串。

總結

在本篇文章當中主要給大家介紹了 python 當中一些基礎的位元組碼對應的含義以及示例程式碼,本篇文章最重要的便是從位元組碼的角度解釋了裝飾器的本質原理,這對我們以後使用裝飾器非常有幫助,可以靈活的控制和了解裝飾器其中發生的故事。


本篇文章是深入理解 python 虛擬機器系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演算法與資料結構)知識。

相關文章