1.python中函數的工作原理 python的解釋器,也就是python.exe(c編寫)會用PyEval_EvalFramEx(c函數)運行foo()函數 首先會創建一個棧幀(stack Frame),在棧幀對象的上下文裡面去運行這個位元組碼。 可以嘗試著去列印foo的位元組碼: 關於位元組碼的解釋: ...
1.python中函數的工作原理
def foo():
bar()
def bar():
pass
python的解釋器,也就是python.exe(c編寫)會用PyEval_EvalFramEx(c函數)運行foo()函數
首先會創建一個棧幀(stack Frame),在棧幀對象的上下文裡面去運行這個位元組碼。
import dis
print(dis.dis(foo)) #列印位元組碼
可以嘗試著去列印foo的位元組碼:
關於位元組碼的解釋:
LOAD_GLOBAL:首先導入bar這個函數
CALL_FUNCTION:執行bar函數
POP_TOP:從棧的頂端去把元素列印出來
LOAD_CONST:返回結果,這裡沒有return,就是None
RETURN_VALUE:返回結果
列印bar的位元組碼:
print(dis.dis(bar))
這個位元組碼全局是唯一的,函數是全局唯一的,然後在函數裡面會調用另外一個函數。
當foo調用函數bar,又會創建一個棧幀,然後將這個函數的控制權交給這個棧幀。
所有的棧幀都分配在記憶體中,它不是放在棧的記憶體上,而是放在堆的記憶體上,你不去釋放它就會一直存在我們的記憶體當中。
這就決定了棧幀可以獨立於調用者存在,比如就算函數不存在了,只要有指針指向bar這個棧幀,就可以對其進行控制。
(python中一切皆對象,棧幀也是對象,是一個位元組碼對象)
import inspect
frame = None #保存frame
def foo():
bar()
def bar():
global frame #引入全局變數
frame = inspect.currentframe() #將bar的frame賦給全局變數
foo()
print(frame.f_code.co_name) #bar 函數退出之後,依然可以拿到bar函數的棧幀
caller_frame = frame.f_back
print(caller_frame.f_code.co_name) #foo 也可以拿到foo函數的棧幀
2.生成器的實現原理
在靜態語言中,函數調用的時候是一個棧的形式,函數調用完成之後棧就會被銷毀。
下麵是函數的調用過程:
PyEval_evalFrameEx會創建一個foo的棧幀對象,這個對象裡面有兩個屬性。f_back為None,因為沒有上層函數,f_code指向foo的位元組碼
同時PyEval_evalFrameEx也會創建一個bar的棧幀對象,f_back指向foo,f_code指向bar的位元組碼。
最大的特點就是棧幀對象存在於堆記憶體中,這樣生成器才有實現的可能。
def gen_func():
yield 1
name = "ming"
yield 2
age = 28
return "kebi" #在早期的生成器版本中不能使用return
當python解釋器在讀取gen_fun()這個函數的時候,發現yield關鍵字就會將其標記為生成器函數。
gen_func()
當我們來調用這個函數的時候,就會返回一個生成器對象。
這個生成器對象是將PyFrame做了一層封裝。
在PyFrameObject和PyCodeObject上面又封裝了一層PyGenObject,就是python的生成器對象。
PyGenObject中gi_frame屬性指向PyGrameObject,gi_code屬性指向PyCodeObject。
PyFrameObject又有f_lasti和f_locals屬性。
f_lasti會指向最近執行的這個代碼。
可以嘗試列印位元組碼:
def gen_func():
yield 1
name = "ming"
yield 2
age = 28
return "kebi" #在早期的生成器版本中不能使用return
import dis
gen = gen_func()
print(dis.dis(gen))
查看結果:
這裡面可以看到有兩次yield。當我們每一次對生成器做一次調用的時候,它遇到yield就會停止。
停止了之後,就會記錄f_lasti(位置)和f_locals(變數)這兩個值。
可以嘗試著調用列印取每一個值,f_lasti和f_locals的變化
print(gen.gi_frame.f_lasti) #-1
print(gen.gi_frame.f_locals) #{}
next(gen)
print(gen.gi_frame.f_lasti) #2
print(gen.gi_frame.f_locals) #{}
next(gen)
print(gen.gi_frame.f_lasti) #12
print(gen.gi_frame.f_locals) #{'name':'ming'}
與上方位元組碼是一樣的。
這樣整個生成器對象就存在與堆記憶體中,可以獨立存在,每次執行一次函數,就會生成一個棧幀對象。
我們可以在任何地方,只要能拿到這個棧幀對象就能夠往前走。這也是python中協程的一個理論基礎。
此時我們可以知道為什麼生成器是一個一個返回。
3.pyc文件
當你在執行python代碼的時候,會發現執行目錄下麵會出現.pyc文件。
[root@tuoguan resources]# ls
r1.py r1.pyc r2.py r3.py
[root@tuoguan resources]# cat r1.pyc
¶:]c@s
dZdS(tname_r1N(R(((s/tmp/demo/resources/r1.py<module>s
r1.pyc是一個二進位文件,當執行的文件中存在包的引入就會編譯生成二進位文件。
當python程式運行時,編譯的結果則是保存在位於記憶體中的PyCodeObject中,當Python程式運行結束時,Python解釋器則將PyCodeObject寫回到pyc文件中。
當python程式第二次運行時,首先程式會在硬碟中尋找pyc文件,如果找到,則直接載入,否則就重覆上面的過程。
所以我們應該這樣來定位PyCodeObject和pyc文件,我們說pyc文件其實是PyCodeObject的一種持久化保存方式。