對於 Python 常規函數,都只有一個入口,但會有多個出口如 return 返回或者拋出異常。函數從入口進入會一直運行到 return 語句或者拋出異常,中間不會暫停,函數一直擁有控制權。當運行結束,才將控制權還給調用者。
前文介紹過,當執行 Python 代碼時,會先將代碼編譯成字節碼,然后在虛擬機中解釋執行字節碼,編譯好的字節碼會保存在 .pyc 或 .pyd 擴展名的文件里。在運行時,虛擬機會創建字節碼執行的上下文環境,Python 模擬 C 語言中的運行棧作為運行時的環境,使用PyFrameObject表示運行時的棧,而字節碼會存儲在 PyCodeObject 對象中。
Python 解釋器是基於棧的,其中有三種棧:調用棧 (frame stack)、數據棧 (data stack)、塊棧 (block statck)。
PyFrameObject 存在於調用棧,其中字段 f_back 指向上一級 PyFrameObject,這樣就形成了一個調用鏈。每個調用棧對應函數的一次調用。調用棧中會有自己的數據棧和塊棧,數據棧中會存放字節碼操作的數據,塊棧用於特定的控制流塊,比如循環和異常處理。
打開終端,在命令行輸入 python3
或 ipython
命令打開 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 處執行。
來自實驗樓