python中的函數、生成器的工做原理

1.python中函數的工做原理python

def foo():
    bar()

def bar():
    pass

python的解釋器,也就是python.exe(c編寫)會用PyEval_EvalFramEx(c函數)運行foo()函數
首先會建立一個棧幀(stack Frame),在棧幀對象的上下文裏面去運行這個字節碼。函數

import dis
print(dis.dis(foo))  #打印字節碼

能夠嘗試着去打印foo的字節碼:指針

在這裏插入圖片描述

關於字節碼的解釋:code

LOAD_GLOBAL:首先導入bar這個函數
CALL_FUNCTION:執行bar函數
POP_TOP:從棧的頂端去把元素打印出來
LOAD_CONST:返回結果,這裏沒有return,就是None
RETURN_VALUE:返回結果

打印bar的字節碼:協程

print(dis.dis(bar))

在這裏插入圖片描述

這個字節碼全局是惟一的,函數是全局惟一的,而後在函數裏面會調用另一個函數。對象

當foo調用函數bar,又會建立一個棧幀,而後將這個函數的控制權交給這個棧幀。blog

全部的棧幀都分配在內存中,它不是放在棧的內存上,而是放在堆的內存上,你不去釋放它就會一直存在咱們的內存當中。圖片

這就決定了棧幀能夠獨立於調用者存在,好比就算函數不存在了,只要有指針指向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的一種持久化保存方式。

相關文章
相關標籤/搜索