python函數的執行過程


對於 Python 常規函數,都只有一個入口,但會有多個出口如 return 返回或者拋出異常。函數從入口進入會一直運行到 return 語句或者拋出異常,中間不會暫停,函數一直擁有控制權。當運行結束,才將控制權還給調用者。

前文介紹過,當執行 Python 代碼時,會先將代碼編譯成字節碼,然后在虛擬機中解釋執行字節碼,編譯好的字節碼會保存在 .pyc 或 .pyd 擴展名的文件里。在運行時,虛擬機會創建字節碼執行的上下文環境,Python 模擬 C 語言中的運行棧作為運行時的環境,使用PyFrameObject表示運行時的棧,而字節碼會存儲在 PyCodeObject 對象中。

Python 解釋器是基於棧的,其中有三種棧:調用棧 (frame stack)、數據棧 (data stack)、塊棧 (block statck)。

PyFrameObject 存在於調用棧,其中字段 f_back 指向上一級 PyFrameObject,這樣就形成了一個調用鏈。每個調用棧對應函數的一次調用。調用棧中會有自己的數據棧和塊棧,數據棧中會存放字節碼操作的數據,塊棧用於特定的控制流塊,比如循環和異常處理。

打開終端,在命令行輸入 python3ipython 命令打開 Python 命令行交互解釋器:

如果使用 ipython 需提前安裝,需要在 Python 3 環境下。

pip3 intall ipython
import inspect

# 全局變量
a = 0
x, y = None, None

def fun1():
    b = 1       # 定義局部變量
    global x    # 將變量 x 設為全局變量,因為它會在函數內部被修改
    # inspect 獲取當前棧幀,相當於 PyFrameObject 對象
    x = inspect.currentframe()
    # 打印當前棧幀中運行的行號
    print(x.f_lasti)
    print('running fun1')
    return b

def fun2(d):
    # 局部變量賦值
    c = a
    e = d
    print('running fun2')
    # 調用方法
    fun1()
    global y
    # 獲取當前棧幀
    y = inspect.currentframe()
    f = 2
    return f

import dis

# dis 方法查看函數的字節碼
>>> dis.dis(fun2)

  2           0 LOAD_GLOBAL              0 (a)
              2 STORE_FAST               1 (c)

  3           4 LOAD_FAST                0 (d)
              6 STORE_FAST               2 (e)

  4           8 LOAD_GLOBAL              1 (print)
             10 LOAD_CONST               1 ('running fun2')
             12 CALL_FUNCTION            1
             14 POP_TOP

  5          16 LOAD_GLOBAL              2 (fun1)
             18 CALL_FUNCTION            0
             20 POP_TOP

  7          22 LOAD_GLOBAL              3 (inspect)
             24 LOAD_ATTR                4 (currentframe)
             26 CALL_FUNCTION            0
             28 STORE_GLOBAL             5 (y)

  8          30 LOAD_CONST               2 (2)
             32 STORE_FAST               3 (f)
             34 LOAD_CONST               0 (None)
             36 RETURN_VALUE

fun2 函數的字節碼,每一列分別是:

源碼行號 | 指令在函數中的偏移 | 指令符號 | 指令參數 | 實際參數值(參考)

先來了解一下 Python 方法的執行過程。在代碼運行時,字節碼會存儲在 PyCodeObject 對象中。PyCodeObject 保存了編譯后的靜態信息,在運行時再結合上下文形成一個完整的運行態環境。函數的 code 變量就是指向的 PyCodeObject 對象,可以查看字節碼信息。

>>> fun1.__code__.co_code           # 查看字節碼
b'd\x01}\x00t\x00j\x01\x83\x00a\x02t\x03t\x02j\x04\x83\x01\x01\x00t\x03d\x02\x83\x01\x01\x00|\x00S\x00'  

>>> list(fun1.__code__.co_code)     # 轉換成 list 之后,是由指令符號后面跟着指令參數組成,指令參數根據指令符號不同個數不同
[100, 1, 125, 0, 116, 0, 106, 1, 131, 0, 97, 2, 116, 3, 116, 2, 106, 4, 131, 1, 1, 0, 116, 3, 100, 2, 131, 1, 1, 0, 124, 0, 83, 0]

>>> dis.opname[100]  # dis 模塊的 opname 存放了操作碼
'LOAD_CONST'         # 100, 1 就是相當於 LOAD_GLOBAL 1

>>> dis.opname[125]  
'STORE_FAST'         # 125, 0 就是相當於 STORE_FAST 0

# PyCodeObject對象中存放這當前上下文的數據
>>> fun1.__code__.co_varnames   # 局部變量名的元組
('b',)

>>> fun1.__code__.co_consts     # 局部變量中的常量元組
(None, 1, 'running fun1')

>>> fun1.__code__.co_names      # 名稱的元組
('inspect', 'currentframe', 'x', 'print', 'f_lasti')

>>> fun2.__code__.co_varnames
('d', 'c', 'e', 'f')

>>> fun2.__code__.co_consts
(None, 'running fun2', 2)

>>> fun2.__code__.co_names
('a', 'print', 'fun1', 'inspect', 'currentframe', 'y')

co_code 中存儲了字節碼,字節碼使用二進制方式存儲,節省存儲空間,指令符號是常量對應的,在指令符號后面跟着指令參數,這樣便於操作。

  • co_varnames 包含局部變量名的元組,所有當前局部變量
  • co_consts 包含字節碼所用字面量的元組,局部常量
  • co_names 包含字節碼所用名稱的元組

inspect 可以獲取調用棧的信息,當執行函數時:

# 運行方法 fun1
# f_lasti 記錄當前棧幀中運行的行號
>>> fun1()
16                              
running fun1
1

# 調用棧中存儲了字節碼信息
>>> x.f_code == fun1.__code__   
True

# co_name 是方法名
>>> x.f_code.co_name
'fun1'

# f_locals 存放局部變量的值
>>> x.f_locals
{'b': 1}

# 上一級調用棧
>>> x.f_back.f_code.co_name  
'<module>'

# 調用方法 fun2
>>> fun2(6)
running fun2
24
running fun1
2

>>> y.f_code.co_name
'fun2'

# 上一級調用棧,fun2 函數調用 fun1 函數,所以 fun1 的上一級調用棧是 fun2
>>> x.f_back.f_code.co_name   
'fun2'

>>> y.f_code.co_names
('a', 'print', 'fun1', 'inspect', 'currentframe', 'y')

>>> y.f_code.co_consts
(None, 'running fun2', 2)

# fun2 方法的局部變量
>>> y.f_locals
{'f': 2, 'e': 6, 'c': 0, 'd': 6}

# fun2 中的全局變量存放在 f_globals 中,並且包含內置變量
>>> y.f_globals['a']
0

介紹幾個常用字節碼的意思:

LOAD_GLOBAL 0 (a)

LOAD_GLOBAL 是取 co_names 元組中索引為 0 的值,即 a,再從 f_globals 中查找 a 的值, 將 a 的值壓入數據棧棧頂,即將值 0 壓入棧頂

STORE_FAST 1 (c)

STORE_FAST 是取 co_names 元組中索引為 1 的值,即 c,取出數據棧棧頂的值,即剛剛壓入棧頂的值 0 ,將值存入 f_locals 中對應的 c 值,這樣就完成了 a 到 c 的賦值操作,現在是 {'c': 0}

LOAD_FAST 0 (d)

LOAD_FAST 是取 co_varnames 元組中索引為 0 的值,即 d ,在 f_locals 中查找d的值,將 d 的值 6 壓入數據棧棧頂

STORE_FAST 2 (e)

STORE_FAST 是取 co_names 元組中索引為 2 的值, 即 e,取出棧頂的值,存入 f_locals 中對應的 e 值,即 {'e': 6}

LOAD_GLOBAL 1 (print)

LOAD_CONST 1 ('running fun2')

CALL_FUNCTION 1

POP_TOP

將 print 和 'running fun2' 依次壓入棧頂,CALL_FUNCTION 調用函數,1 是將棧頂的一個數據 ('running fun2') 彈出作為下一個函數調用的參數,然后彈出 print ,調用 print 函數。執行函數 print('running fun2')

LOAD_FAST 3 (f)

RETURN_VALUE

將f的值壓入棧頂,RETURN_VALUE 將棧頂的值取出,作為函數返回的值,傳給上一級的調用棧,開始運行上一級的調用棧。

Python 中函數執行過程和數據存儲是分開的。函數在調用執行時依據調用棧,每個調用棧都有自己的數據棧,數據存放在數據棧中,調用棧是解釋器在堆上分配內存,所以在函數執行結束之后,棧幀還存在,數據還保留。在執行 CALL_FUNCTION 調用其他的函數時,棧幀會使用 f_lasti 記錄下執行的行號,在函數返回時繼續從 f_lasti 處執行。

來自實驗樓

https://www.shiyanlou.com/courses/1292/learning/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM